From 5d1ed48dfc8e0d6a9fa9afe137e18071c190e1b8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 2 Sep 2021 13:11:04 +0200 Subject: [PATCH 001/259] Ensure setExposureOffset returns new value on Android (#4301) --- packages/camera/camera/CHANGELOG.md | 4 ++++ .../src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- .../test/java/io/flutter/plugins/camera/CameraTest.java | 4 +++- packages/camera/camera/example/lib/main.dart | 7 +++++++ packages/camera/camera/pubspec.yaml | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 5a3a1bf251d7..b141fab62595 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2+2 + +* Ensure that setting the exposure offset returns the new offset value on Android. + ## 0.9.2+1 * Fixed camera controller throwing an exception when being replaced in the preview widget. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index c036c1c7e9d3..4601e7d34d69 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -906,7 +906,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) { exposureOffsetFeature.updateBuilder(previewRequestBuilder); refreshPreviewCaptureSession( - () -> result.success(null), + () -> result.success(exposureOffsetFeature.getValue()), (code, message) -> result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 5431df0df636..fbed28bc11fc 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -687,11 +687,13 @@ public void setExposureOffset_shouldUpdateExposureOffsetFeature() { mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockExposureOffsetFeature.getValue()).thenReturn(1.0); + camera.setExposureOffset(mockResult, 1.0); verify(mockExposureOffsetFeature, times(1)).setValue(1.0); verify(mockResult, never()).error(any(), any(), any()); - verify(mockResult, times(1)).success(null); + verify(mockResult, times(1)).success(1.0); } @Test diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index a8067001aae5..c0e90eefa3ab 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -399,6 +399,13 @@ class _CameraExampleHomeState extends State onSetExposureModeButtonPressed(ExposureMode.locked) : null, ), + TextButton( + child: Text('RESET OFFSET'), + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, + ), ], ), Center( diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 400b8c03f44a..582a830ebb4c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.2+1 +version: 0.9.2+2 environment: sdk: ">=2.12.0 <3.0.0" From 6f037b43ff0feaf7b52299b373cb02960e751cae Mon Sep 17 00:00:00 2001 From: Kyle Finlinson <5882840+KyleFin@users.noreply.github.com> Date: Thu, 2 Sep 2021 10:46:03 -0600 Subject: [PATCH 002/259] [video_player] Ensure seekTo is not called before video player is initialized. (#4300) --- .../video_player/video_player/CHANGELOG.md | 3 ++- .../video_player/lib/video_player.dart | 12 +++++----- .../video_player/video_player/pubspec.yaml | 2 +- .../video_player/test/video_player_test.dart | 22 +++++++++++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f07bb5f66f8c..9cb642a4db56 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.1.15 +* Ensured seekTo isn't called before video player is initialized. Fixes [#89259](https://github.com/flutter/flutter/issues/89259). * Updated Android lint settings. ## 2.1.14 diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 772409258ac4..b4c4b2b2a311 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -415,14 +415,14 @@ class VideoPlayerController extends ValueNotifier { } Future _applyLooping() async { - if (!value.isInitialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } await _videoPlayerPlatform.setLooping(_textureId, value.isLooping); } Future _applyPlayPause() async { - if (!value.isInitialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } if (value.isPlaying) { @@ -455,14 +455,14 @@ class VideoPlayerController extends ValueNotifier { } Future _applyVolume() async { - if (!value.isInitialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } await _videoPlayerPlatform.setVolume(_textureId, value.volume); } Future _applyPlaybackSpeed() async { - if (!value.isInitialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } @@ -491,7 +491,7 @@ class VideoPlayerController extends ValueNotifier { /// If [moment] is outside of the video's full range it will be automatically /// and silently clamped. Future seekTo(Duration position) async { - if (_isDisposed) { + if (_isDisposedOrNotInitialized) { return; } if (position > value.duration) { @@ -572,6 +572,8 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith(position: position); value = value.copyWith(caption: _getCaptionAt(position)); } + + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; } class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 0d0cdb1cb436..7f6f608687cc 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.14 +version: 2.1.15 environment: sdk: ">=2.12.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 b5bfad605620..ad536f840c1d 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -318,6 +318,17 @@ void main() { expect(fakeVideoPlayerPlatform.calls.last, 'setPlaybackSpeed'); }); + test('play before initialized does not call platform', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + expect(controller.value.isInitialized, isFalse); + + await controller.play(); + + expect(fakeVideoPlayerPlatform.calls, isEmpty); + }); + test('play restarts from beginning if video is at end', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', @@ -373,6 +384,17 @@ void main() { expect(await controller.position, const Duration(milliseconds: 500)); }); + test('before initialized does not call platform', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + expect(controller.value.isInitialized, isFalse); + + await controller.seekTo(const Duration(milliseconds: 500)); + + expect(fakeVideoPlayerPlatform.calls, isEmpty); + }); + test('clamps values that are too high or low', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', From 887ba2d8f1350bd4f9c2e1082077baa2b681a4c3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 2 Sep 2021 13:04:25 -0400 Subject: [PATCH 003/259] Add scripts for Windows LUCI recipe (#4303) See https://github.com/flutter/flutter/issues/89373 for context. This sets up the script and config file to replicate the current plugins.py recipe's behavior using the planned generic recipe. These are currently unused, but are being landed first so that the generic recipe has something to test against. Part of https://github.com/flutter/flutter/issues/89373 --- .ci.yaml | 2 ++ .ci/scripts/build_examples_win32.sh | 7 +++++++ .ci/scripts/drive_examples_win32.sh | 7 +++++++ .ci/scripts/prepare_tool.sh | 10 ++++++++++ .ci/targets/windows_build_and_platform_tests.yaml | 8 ++++++++ 5 files changed, 34 insertions(+) create mode 100644 .ci/scripts/build_examples_win32.sh create mode 100644 .ci/scripts/drive_examples_win32.sh create mode 100644 .ci/scripts/prepare_tool.sh create mode 100644 .ci/targets/windows_build_and_platform_tests.yaml diff --git a/.ci.yaml b/.ci.yaml index 6b5c385aa98e..460badfbdf12 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -39,6 +39,7 @@ targets: recipe: plugins/plugins timeout: 30 properties: + target_file: windows_build_and_platform_tests.yaml dependencies: > [ {"dependency": "vs_build"} @@ -49,6 +50,7 @@ targets: recipe: plugins/plugins timeout: 30 properties: + target_file: windows_build_and_platform_tests.yaml channel: stable dependencies: > [ diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh new file mode 100644 index 000000000000..8c090f4b78d2 --- /dev/null +++ b/.ci/scripts/build_examples_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ + --packages-for-branch diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh new file mode 100644 index 000000000000..63abc06bec5a --- /dev/null +++ b/.ci/scripts/drive_examples_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ + --packages-for-branch diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh new file mode 100644 index 000000000000..1095e2189a36 --- /dev/null +++ b/.ci/scripts/prepare_tool.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# To set FETCH_HEAD for "git merge-base" to work +git fetch origin master + +cd script/tool +dart pub get diff --git a/.ci/targets/windows_build_and_platform_tests.yaml b/.ci/targets/windows_build_and_platform_tests.yaml new file mode 100644 index 000000000000..cba120073310 --- /dev/null +++ b/.ci/targets/windows_build_and_platform_tests.yaml @@ -0,0 +1,8 @@ + +tasks: + - name: "prepare tool" + script: .ci/scripts/prepare_tool.sh + - name: "build examples" + script: .ci/scripts/build_examples_win32.sh + - name: "drive examples" + script: .ci/scripts/drive_examples_win32.sh From 2aa9a0947c6172e02d745631d2de6c8a08743554 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Thu, 2 Sep 2021 15:36:03 -0700 Subject: [PATCH 004/259] [ci.yaml] Add builders to recipes cq (#4306) --- .ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 460badfbdf12..ebedd203b3ca 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -39,6 +39,7 @@ targets: recipe: plugins/plugins timeout: 30 properties: + add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml dependencies: > [ @@ -50,6 +51,7 @@ targets: recipe: plugins/plugins timeout: 30 properties: + add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml channel: stable dependencies: > From 08573317673f7cf3257199871b0f4a0db50622af Mon Sep 17 00:00:00 2001 From: ThetaSinner Date: Fri, 3 Sep 2021 05:46:02 +0100 Subject: [PATCH 005/259] adds option to re-authenticate on google silent sign in (#4251) --- .../google_sign_in/google_sign_in/CHANGELOG.md | 4 +++- .../google_sign_in/lib/google_sign_in.dart | 9 ++++++--- .../google_sign_in/google_sign_in/pubspec.yaml | 2 +- .../google_sign_in/test/google_sign_in_test.dart | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 8ac07ae1793b..1f0be2e237b2 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,4 +1,6 @@ -## NEXT +## 5.1.0 + +* Add reAuthenticate option to signInSilently to allow re-authentication to be requested * Updated Android lint settings. 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 b45b09c2d7a7..04d60fbc7d21 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -314,11 +314,13 @@ class GoogleSignIn { /// successful sign in or `null` if there is no previously authenticated user. /// Use [signIn] method to trigger interactive sign in process. /// - /// Authentication process is triggered only if there is no currently signed in + /// Authentication is triggered if there is no currently signed in /// user (that is when `currentUser == null`), otherwise this method returns /// a Future which resolves to the same user instance. /// - /// Re-authentication can be triggered only after [signOut] or [disconnect]. + /// Re-authentication can be triggered after [signOut] or [disconnect]. It can + /// also be triggered by setting [reAuthenticate] to `true` if a new ID token + /// is required. /// /// When [suppressErrors] is set to `false` and an error occurred during sign in /// returned Future completes with [PlatformException] whose `code` can be @@ -327,10 +329,11 @@ class GoogleSignIn { /// (when an unknown error occurred). Future signInSilently({ bool suppressErrors = true, + bool reAuthenticate = false, }) async { try { return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently, - canSkipCall: true); + canSkipCall: !reAuthenticate); } catch (_) { if (suppressErrors) { return null; diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 7e3f221716a8..79009373c5d1 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/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 and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.0.7 +version: 5.1.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index f642bcd2eaf8..444edc4336ce 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -281,6 +281,22 @@ void main() { throwsA(isInstanceOf())); }); + test('signInSilently allows re-authentication to be requested', () async { + await googleSignIn.signInSilently(); + expect(googleSignIn.currentUser, isNotNull); + + await googleSignIn.signInSilently(reAuthenticate: true); + + expect( + log, + [ + _isSignInMethodCall(), + isMethodCall('signInSilently', arguments: null), + isMethodCall('signInSilently', arguments: null), + ], + ); + }); + test('can sign in after init failed before', () async { int initCount = 0; channel.setMockMethodCallHandler((MethodCall methodCall) { From 597771f5c6d4621d5346c86466b44c847aa529b7 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 3 Sep 2021 12:51:04 -0700 Subject: [PATCH 006/259] [camera_web] Make plugin publishable for the first time. (#4304) --- packages/camera/camera_web/CHANGELOG.md | 9 +--- packages/camera/camera_web/README.md | 55 +++++++++++++++++-------- packages/camera/camera_web/pubspec.yaml | 8 +--- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index a481554b540c..b116636c2808 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,8 +1,3 @@ -## 0.1.0+1 +## 0.2.0 -* Add `implements` to pubspec. - -## 0.1.0 - -* Initial release - * Added CameraOptions used to constrain the camera audio and video. +* Initial release, adapted from the Flutter [I/O Photobooth](https://photobooth.flutter.dev/) project. diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index 8c216b3f4e0e..c6e1e0f13cab 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -6,7 +6,10 @@ The web implementation of [`camera`][camera]. ## Usage -This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `camera` normally. This package will be automatically included in your app when you do. +### Depend on the package + +This package is not an [endorsed implementation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin) +of the google_maps_flutter plugin yet, so you'll need to [add it explicitly](https://pub.dev/packages/camera_web/install). ## Example @@ -16,41 +19,59 @@ Find the example in the [`camera` package](https://pub.dev/packages/camera#examp ### Camera devices -The camera devices are accessed with [Stream Web API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) with the following [browser support](https://caniuse.com/stream): +The camera devices are accessed with [Stream Web API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) +with the following [browser support](https://caniuse.com/stream): ![Data on support for the Stream feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/stream.png) -Accessing camera devices requires a [secure browsing context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). This means that you might need to serve your web application over HTTPS. For insecure contexts `CameraPlatform.availableCameras` might throw a `CameraException` with the `permissionDenied` error code. +Accessing camera devices requires a [secure browsing context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). +Broadly speaking, this means that you need to serve your web application over HTTPS +(or `localhost` for local development). For insecure contexts +`CameraPlatform.availableCameras` might throw a `CameraException` with the +`permissionDenied` error code. ### Device orientation -The device orientation implementation is backed by [`Screen Orientation Web API`](https://www.w3.org/TR/screen-orientation/) with the following [browser support](https://caniuse.com/screen-orientation): +The device orientation implementation is backed by [`Screen Orientation Web API`](https://www.w3.org/TR/screen-orientation/) +with the following [browser support](https://caniuse.com/screen-orientation): ![Data on support for the Screen Orientation feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/screen-orientation.png) For the browsers that do not support the device orientation: + - `CameraPlatform.onDeviceOrientationChanged` returns an empty stream. -- `CameraPlatform.lockCaptureOrientation` and `CameraPlatform.unlockCaptureOrientation` throw a `PlatformException` with the `orientationNotSupported` error code. +- `CameraPlatform.lockCaptureOrientation` and `CameraPlatform.unlockCaptureOrientation` +throw a `PlatformException` with the `orientationNotSupported` error code. ### Flash mode and zoom level -The flash mode and zoom level implementation is backed by [Image Capture Web API](https://w3c.github.io/mediacapture-image/) with the following [browser support](https://caniuse.com/mdn-api_imagecapture) (as of 12 August 2021): +The flash mode and zoom level implementation is backed by [Image Capture Web API](https://w3c.github.io/mediacapture-image/) +with the following [browser support](https://caniuse.com/mdn-api_imagecapture): -![Data on support for the Image Capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/static/v1/mdn-api__ImageCapture-1628778966589.png) +![Data on support for the Image Capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/static/v1/mdn-api__ImageCapture-1628778966589.png) For the browsers that do not support the flash mode: -- `CameraPlatform.setFlashMode` throws a `PlatformException` with the `torchModeNotSupported` error code. + +- `CameraPlatform.setFlashMode` throws a `PlatformException` with the +`torchModeNotSupported` error code. For the browsers that do not support the zoom level: -- `CameraPlatform.getMaxZoomLevel`, `CameraPlatform.getMinZoomLevel` and `CameraPlatform.setZoomLevel` throw a `PlatformException` with the `zoomLevelNotSupported` error code. + +- `CameraPlatform.getMaxZoomLevel`, `CameraPlatform.getMinZoomLevel` and +`CameraPlatform.setZoomLevel` throw a `PlatformException` with the +`zoomLevelNotSupported` error code. ### Taking a picture -The image capturing implementation is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) with the following [browser support](https://caniuse.com/bloburls): +The image capturing implementation is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) +with the following [browser support](https://caniuse.com/bloburls): ![Data on support for the Blob URLs feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) -The web platform does not support `dart:io`. Attempts to display a captured image using `Image.file` will throw an error. The capture image contains a network-accessible URL pointing to a location within the browser and should be displayed using `Image.network` or `Image.memory` after loading the image bytes to memory. +The web platform does not support `dart:io`. Attempts to display a captured image +using `Image.file` will throw an error. The capture image contains a network-accessible +URL pointing to a location within the browser (blob) and can be displayed using +`Image.network` or `Image.memory` after loading the image bytes to memory. See the example below: @@ -65,13 +86,13 @@ if (kIsWeb) { ## Missing implementation The web implementation of [`camera`][camera] is missing the following features: -- Video recording + +- Video recording ([in progress](https://github.com/flutter/plugins/pull/4210)) - Exposure mode, point and offset - Focus mode and point -- Camera closing events -- Camera sensor orientation -- Camera image format group -- Camera image streaming +- Sensor orientation +- Image format group +- Streaming of frames -[camera]: https://pub.dev/packages/camera \ No newline at end of file +[camera]: https://pub.dev/packages/camera diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 70194d9037d4..fdfe3e38bb98 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,12 +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/master/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.1.0+1 - -# This plugin is under development and will be published -# when the first working web camera implementation is added. -# TODO(bselwe): Remove when camera_web should be published. -publish_to: none +version: 0.2.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -27,6 +22,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + stream_transform: ^2.0.0 dev_dependencies: flutter_test: From 189a45eeb89465366051ea54d2fc14d70681be10 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 3 Sep 2021 15:51:04 -0700 Subject: [PATCH 007/259] build-examples .pluginToolsConfig.yaml support (#4305) --- script/tool/CHANGELOG.md | 4 + .../tool/lib/src/build_examples_command.dart | 74 ++++++++++++++++++- script/tool/pubspec.yaml | 2 +- .../test/build_examples_command_test.dart | 48 +++++++++++- 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 098e57a8c62d..aa73c65f3e80 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.1 + +- Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. + ## 0.7.0 - `native-test` now supports `--linux` for unit tests. diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 56c2f5c7dc87..82ed074c462a 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -16,7 +17,19 @@ import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; +const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; +const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; +const String _pluginToolsConfigGlobalKey = 'global'; + +const String _pluginToolsConfigExample = ''' +$_pluginToolsConfigBuildFlagsKey: + $_pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" +'''; + const int _exitNoPlatformFlags = 3; +const int _exitInvalidPluginToolsConfig = 4; // Flutter build types. These are the values passed to `flutter build `. const String _flutterBuildTypeAndroid = 'apk'; @@ -99,7 +112,13 @@ class BuildExamplesCommand extends PackageLoopingCommand { @override final String description = 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.'; + 'This command requires "flutter" to be in your path.\n\n' + 'A $_pluginToolsConfigFileName file can be placed in an example app ' + 'directory to specify additional build arguments. It should be a YAML ' + 'file with a top-level map containing a single key ' + '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' + 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' + 'arguments.'; @override Future initializeRun() async { @@ -202,6 +221,58 @@ class BuildExamplesCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } + Iterable _readExtraBuildFlagsConfiguration( + Directory directory) sync* { + final File pluginToolsConfig = + directory.childFile(_pluginToolsConfigFileName); + if (pluginToolsConfig.existsSync()) { + final Object? configuration = + loadYaml(pluginToolsConfig.readAsStringSync()); + if (configuration is! YamlMap) { + printError('The $_pluginToolsConfigFileName file must be a YAML map.'); + printError( + 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); + printError( + 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); + printError( + 'has any effect; it must contain a list of arguments to pass to the'); + printError('flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { + final Object? buildFlagsConfiguration = + configuration[_pluginToolsConfigBuildFlagsKey]; + if (buildFlagsConfiguration is! YamlMap) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); + printError( + 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); + printError( + 'contain a list of arguments to pass to the flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { + final Object? globalBuildFlagsConfiguration = + buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; + if (globalBuildFlagsConfiguration is! YamlList) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); + printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); + printError( + 'That list must contain a list of arguments to pass to the flutter tool.'); + printError( + 'For example, the $_pluginToolsConfigFileName file could look like:'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + yield* globalBuildFlagsConfiguration.cast(); + } + } + } + } + Future _buildExample( RepositoryPackage example, String flutterBuildType, { @@ -231,6 +302,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { 'build', flutterBuildType, ...extraBuildFlags, + ..._readExtraBuildFlagsConfiguration(example.directory), if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 2569e0ede870..689618f06123 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/master/script/tool -version: 0.7.0 +version: 0.7.1 dependencies: args: ^2.1.0 diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index d9cbad246d28..c3b0cb9d5cd1 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -379,7 +379,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); // Output should be empty since running build-examples --macos with no macos // implementation is a no-op. expect(processRunner.recordedCalls, orderedEquals([])); @@ -407,7 +406,6 @@ void main() { ]), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, containsAll([ @@ -436,7 +434,6 @@ void main() { contains('Creating temporary winuwp folder'), ); - print(processRunner.recordedCalls); expect( processRunner.recordedCalls, orderedEquals([ @@ -679,5 +676,50 @@ void main() { ])); }); }); + + test('The .pluginToolsConfig.yaml file', () async { + mockPlatform.isLinux = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final File pluginExampleConfigFile = + pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); + pluginExampleConfigFile + .writeAsStringSync('buildFlags:\n global:\n - "test argument"'); + + final List output = [ + ...await runCapturingPrint( + runner, ['build-examples', '--linux']), + ...await runCapturingPrint( + runner, ['build-examples', '--macos']), + ]; + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Linux', + '\nBUILDING plugin/example for macOS', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux', 'test argument'], + pluginExampleDirectory.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'macos', 'test argument'], + pluginExampleDirectory.path), + ])); + }); }); } From 17c41495b97514fb599233582efb3d2ab4a0eeef Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 3 Sep 2021 18:56:06 -0400 Subject: [PATCH 008/259] Remove gradle.properties from plugins (#4309) --- packages/android_alarm_manager/android/gradle.properties | 1 - packages/android_intent/android/gradle.properties | 1 - packages/battery/battery/android/gradle.properties | 1 - packages/camera/camera/android/gradle.properties | 1 - packages/connectivity/connectivity/android/gradle.properties | 1 - packages/device_info/device_info/android/gradle.properties | 1 - packages/espresso/android/gradle.properties | 4 ---- .../android/gradle.properties | 4 ---- .../google_maps_flutter/android/gradle.properties | 4 ---- .../google_sign_in/google_sign_in/android/gradle.properties | 4 ---- packages/image_picker/image_picker/android/gradle.properties | 1 - .../in_app_purchase_android/android/gradle.properties | 1 - packages/local_auth/android/gradle.properties | 1 - packages/package_info/android/gradle.properties | 1 - .../path_provider/path_provider/android/gradle.properties | 1 - .../quick_actions/quick_actions/android/gradle.properties | 1 - packages/sensors/android/gradle.properties | 1 - packages/share/android/gradle.properties | 1 - .../shared_preferences/android/gradle.properties | 1 - packages/url_launcher/url_launcher/android/gradle.properties | 1 - packages/video_player/video_player/android/gradle.properties | 1 - .../wifi_info_flutter/android/gradle.properties | 3 --- 22 files changed, 36 deletions(-) delete mode 100644 packages/android_alarm_manager/android/gradle.properties delete mode 100644 packages/android_intent/android/gradle.properties delete mode 100644 packages/battery/battery/android/gradle.properties delete mode 100644 packages/camera/camera/android/gradle.properties delete mode 100644 packages/connectivity/connectivity/android/gradle.properties delete mode 100644 packages/device_info/device_info/android/gradle.properties delete mode 100644 packages/espresso/android/gradle.properties delete mode 100644 packages/flutter_plugin_android_lifecycle/android/gradle.properties delete mode 100644 packages/google_maps_flutter/google_maps_flutter/android/gradle.properties delete mode 100644 packages/google_sign_in/google_sign_in/android/gradle.properties delete mode 100755 packages/image_picker/image_picker/android/gradle.properties delete mode 100644 packages/in_app_purchase/in_app_purchase_android/android/gradle.properties delete mode 100644 packages/local_auth/android/gradle.properties delete mode 100644 packages/package_info/android/gradle.properties delete mode 100644 packages/path_provider/path_provider/android/gradle.properties delete mode 100644 packages/quick_actions/quick_actions/android/gradle.properties delete mode 100644 packages/sensors/android/gradle.properties delete mode 100644 packages/share/android/gradle.properties delete mode 100644 packages/shared_preferences/shared_preferences/android/gradle.properties delete mode 100644 packages/url_launcher/url_launcher/android/gradle.properties delete mode 100644 packages/video_player/video_player/android/gradle.properties delete mode 100644 packages/wifi_info_flutter/wifi_info_flutter/android/gradle.properties diff --git a/packages/android_alarm_manager/android/gradle.properties b/packages/android_alarm_manager/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/android_alarm_manager/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/android_intent/android/gradle.properties b/packages/android_intent/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/android_intent/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/battery/battery/android/gradle.properties b/packages/battery/battery/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/battery/battery/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/camera/camera/android/gradle.properties b/packages/camera/camera/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/camera/camera/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/connectivity/connectivity/android/gradle.properties b/packages/connectivity/connectivity/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/connectivity/connectivity/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/device_info/device_info/android/gradle.properties b/packages/device_info/device_info/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/device_info/device_info/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/espresso/android/gradle.properties b/packages/espresso/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/espresso/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/flutter_plugin_android_lifecycle/android/gradle.properties b/packages/flutter_plugin_android_lifecycle/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/flutter_plugin_android_lifecycle/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/google_maps_flutter/google_maps_flutter/android/gradle.properties b/packages/google_maps_flutter/google_maps_flutter/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/google_sign_in/google_sign_in/android/gradle.properties b/packages/google_sign_in/google_sign_in/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/google_sign_in/google_sign_in/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/image_picker/image_picker/android/gradle.properties b/packages/image_picker/image_picker/android/gradle.properties deleted file mode 100755 index 8bd86f680510..000000000000 --- a/packages/image_picker/image_picker/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/in_app_purchase/in_app_purchase_android/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/local_auth/android/gradle.properties b/packages/local_auth/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/local_auth/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/package_info/android/gradle.properties b/packages/package_info/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/package_info/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/path_provider/path_provider/android/gradle.properties b/packages/path_provider/path_provider/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/path_provider/path_provider/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/quick_actions/quick_actions/android/gradle.properties b/packages/quick_actions/quick_actions/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/quick_actions/quick_actions/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/sensors/android/gradle.properties b/packages/sensors/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/sensors/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/share/android/gradle.properties b/packages/share/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/share/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/shared_preferences/shared_preferences/android/gradle.properties b/packages/shared_preferences/shared_preferences/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/shared_preferences/shared_preferences/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/url_launcher/url_launcher/android/gradle.properties b/packages/url_launcher/url_launcher/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/url_launcher/url_launcher/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/video_player/video_player/android/gradle.properties b/packages/video_player/video_player/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/video_player/video_player/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle.properties b/packages/wifi_info_flutter/wifi_info_flutter/android/gradle.properties deleted file mode 100644 index 94adc3a3f97a..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true From 300100b34397f4023f5e72faa60b863dfe4a3d08 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 3 Sep 2021 19:01:04 -0400 Subject: [PATCH 009/259] [flutter_plugin_tools] Adjust diff logging (#4312) --- .../lib/src/common/git_version_finder.dart | 21 +++++++++++-------- .../tool/lib/src/common/plugin_command.dart | 3 +++ .../tool/lib/src/publish_plugin_command.dart | 3 +++ .../tool/test/common/plugin_command_test.dart | 9 +++++++- .../test/publish_plugin_command_test.dart | 2 ++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 2c9519e7a856..a0a7a32b5e0f 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -11,7 +11,7 @@ import 'package:yaml/yaml.dart'; /// Finding diffs based on `baseGitDir` and `baseSha`. class GitVersionFinder { /// Constructor - GitVersionFinder(this.baseGitDir, this.baseSha); + GitVersionFinder(this.baseGitDir, String? baseSha) : _baseSha = baseSha; /// The top level directory of the git repo. /// @@ -19,7 +19,7 @@ class GitVersionFinder { final GitDir baseGitDir; /// The base sha used to get diff. - final String? baseSha; + String? _baseSha; static bool _isPubspec(String file) { return file.trim().endsWith('pubspec.yaml'); @@ -32,10 +32,9 @@ class GitVersionFinder { /// Get a list of all the changed files. Future> getChangedFiles() async { - final String baseSha = await _getBaseSha(); + final String baseSha = await getBaseSha(); final io.ProcessResult changedFilesCommand = await baseGitDir .runCommand(['diff', '--name-only', baseSha, 'HEAD']); - print('Determine diff with base sha: $baseSha'); final String changedFilesStdout = changedFilesCommand.stdout.toString(); if (changedFilesStdout.isEmpty) { return []; @@ -49,7 +48,7 @@ class GitVersionFinder { /// at the revision of `gitRef` (defaulting to the base if not provided). Future getPackageVersion(String pubspecPath, {String? gitRef}) async { - final String ref = gitRef ?? (await _getBaseSha()); + final String ref = gitRef ?? (await getBaseSha()); io.ProcessResult gitShow; try { @@ -63,9 +62,11 @@ class GitVersionFinder { return versionString == null ? null : Version.parse(versionString); } - Future _getBaseSha() async { - if (baseSha != null && baseSha!.isNotEmpty) { - return baseSha!; + /// Returns the base used to diff against. + Future getBaseSha() async { + String? baseSha = _baseSha; + if (baseSha != null && baseSha.isNotEmpty) { + return baseSha; } io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( @@ -76,6 +77,8 @@ class GitVersionFinder { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); } - return (baseShaFromMergeBase.stdout as String).trim(); + baseSha = (baseShaFromMergeBase.stdout as String).trim(); + _baseSha = baseSha; + return baseSha; } } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 514a90b85cc7..5d5cbd9abf6c 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -314,6 +314,9 @@ abstract class PluginCommand extends Command { if (runOnChangedPackages) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Running for all packages that have changed relative to "$baseSha"\n'); final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6da51706ef1e..769b9e8c8f00 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -159,6 +159,9 @@ class PublishPluginCommand extends PackageLoopingCommand { Stream getPackagesToProcess() async* { if (getBoolArg(_allChangedFlag)) { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Publishing all packages that have changed relative to "$baseSha"\n'); final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 3ef0d3b3c005..13724e26e5f8 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -398,12 +398,19 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ + final List output = await runCapturingPrint(runner, [ 'sample', '--base-sha=master', '--run-on-changed-packages' ]); + expect( + output, + containsAllInOrder([ + contains( + 'Running for all packages that have changed relative to "master"'), + ])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 2ea4fc753460..14e99a10f365 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -466,6 +466,8 @@ void main() { expect( output, containsAllInOrder([ + contains( + 'Publishing all packages that have changed relative to "HEAD~"'), contains('Running `pub publish ` in ${pluginDir1.path}...'), contains('Running `pub publish ` in ${pluginDir2.path}...'), contains('plugin1 - \x1B[32mpublished\x1B[0m'), From 998f51f8e8abc6a31366dd3338590c2e2ff99e06 Mon Sep 17 00:00:00 2001 From: Christopher Boyd Date: Sun, 5 Sep 2021 00:26:04 -0400 Subject: [PATCH 010/259] [image_picker] add forceFullMetadata to interface (#4288) --- .../lib/image_picker_for_web.dart | 2 + .../CHANGELOG.md | 8 +++ .../method_channel_image_picker.dart | 6 ++ .../image_picker_platform.dart | 12 ++++ .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 58 +++++++++++++------ 6 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index b170ee3256ab..e1ade78d997d 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -53,6 +53,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { String? capture = computeCaptureAttribute(source, preferredCameraDevice); @@ -115,6 +116,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? capture = computeCaptureAttribute(source, preferredCameraDevice); diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 97480e044284..3c14e404e4d1 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.4.0 + +* Add `forceFullMetadata` option to `pickImage`. + * To keep this non-breaking `forceFullMetadata` defaults to `true`, so the plugin tries + to get the full image metadata which may require extra permission requests on certain platforms. + * If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces + permission requests from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + ## 2.3.0 * Updated `LostDataResponse` to include a `files` property, in case more than one file was recovered. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index b02284e957fa..082b40357a1f 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -24,6 +24,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -31,6 +32,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? PickedFile(path) : null; @@ -85,6 +87,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { @@ -107,6 +110,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, + 'forceFullMetadata': forceFullMetadata, 'cameraDevice': preferredCameraDevice.index }, ); @@ -183,6 +187,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -190,6 +195,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? XFile(path) : null; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index 5c1c8b698442..60fa784c22a3 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -58,6 +58,11 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g. on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -73,6 +78,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('pickImage() has not been implemented.'); @@ -164,6 +170,11 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g. on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -179,6 +190,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('getImage() has not been implemented.'); diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 2168ff0f778a..53852de4f1d3 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.3.0 +version: 2.4.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index 17caa8456621..a2d9568fc85d 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -40,14 +40,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -93,49 +95,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -196,6 +205,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -215,6 +225,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], ); @@ -509,14 +520,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -562,49 +575,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -664,6 +684,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -683,6 +704,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], ); From 3a38cd09a736ebbd8ba6b29a1ceeb5dffd628fbd Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Sun, 5 Sep 2021 17:24:24 -0700 Subject: [PATCH 011/259] Revert "[image_picker] add forceFullMetadata to interface" (#4314) Reverts #4288 which published a breaking change as if it were a non-breaking change. The discussion in that PR prior to it landing was incorrect, because adding a new parameter with a default value is non-breaking *only for clients*. It is breaking for subclasses that override it, and the purpose of the platform interface is for implementations to subclass it and override everything. --- .../lib/image_picker_for_web.dart | 2 - .../CHANGELOG.md | 5 ++ .../method_channel_image_picker.dart | 6 -- .../image_picker_platform.dart | 12 ---- .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 58 ++++++------------- 6 files changed, 24 insertions(+), 61 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index e1ade78d997d..b170ee3256ab 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -53,7 +53,6 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { String? capture = computeCaptureAttribute(source, preferredCameraDevice); @@ -116,7 +115,6 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? capture = computeCaptureAttribute(source, preferredCameraDevice); diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 3c14e404e4d1..d637ac1a277e 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.4.1 + +* Reverts the changes from 2.4.0, which was a breaking change that + was incorrectly marked as a non-breaking change. + ## 2.4.0 * Add `forceFullMetadata` option to `pickImage`. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 082b40357a1f..b02284e957fa 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -24,7 +24,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -32,7 +31,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, - forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? PickedFile(path) : null; @@ -87,7 +85,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { @@ -110,7 +107,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, - 'forceFullMetadata': forceFullMetadata, 'cameraDevice': preferredCameraDevice.index }, ); @@ -187,7 +183,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -195,7 +190,6 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, - forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? XFile(path) : null; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index 60fa784c22a3..5c1c8b698442 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -58,11 +58,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// - /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require - /// extra permission requests on certain platforms. - /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests - /// from the platform (e.g. on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). - /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -78,7 +73,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('pickImage() has not been implemented.'); @@ -170,11 +164,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// - /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require - /// extra permission requests on certain platforms. - /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests - /// from the platform (e.g. on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). - /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -190,7 +179,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, - bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('getImage() has not been implemented.'); diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 53852de4f1d3..e41137fcb06b 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.4.0 +version: 2.4.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index a2d9568fc85d..17caa8456621 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -40,16 +40,14 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), ], ); @@ -95,56 +93,49 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), ], ); @@ -205,7 +196,6 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, - 'forceFullMetadata': true, }), ], ); @@ -225,7 +215,6 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, - 'forceFullMetadata': true, }), ], ); @@ -520,16 +509,14 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), ], ); @@ -575,56 +562,49 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0, - 'forceFullMetadata': true, + 'cameraDevice': 0 }), ], ); @@ -684,7 +664,6 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, - 'forceFullMetadata': true, }), ], ); @@ -704,7 +683,6 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, - 'forceFullMetadata': true, }), ], ); From ae92e6246a27212db04d29c9ba4a4d85cdaa810e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Sep 2021 14:52:02 -0400 Subject: [PATCH 012/259] Allow neutral conclusion in publishing check (#4321) It's possible for jobs to conclude in a "neutral" state; see https://github.com/flutter/plugins/runs/3534917860 for instance. Currently this causes "release" to be red when it shouldn't be. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f1a4a360949..2393eadab4c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds - allowed-conclusions: success + allowed-conclusions: success,neutral # verbose:true will produce too many logs that hang github actions web UI. verbose: false From 98481816b1a13af09528d9353c80de4f0412cca5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 8 Sep 2021 10:05:17 -0400 Subject: [PATCH 013/259] Bring up new Windows test targets (#4311) Uses the new generic plugins recipe to add a number of new Windows tests that have recently gained tool support, but not yet added to the LUCI builds: - New targets: - The "build all plugins" test (master and stable) - UWP build tests (master only, since UWP is not on stable yet) - Tool tests (master only with existing tool tests on Linux) - Replacement versions of the existing builders but with names that match the Cirrus naming to make the parallels in testing easier to understand in the GitHub UI and configs. - Modification of existing targets: - Adds Windows native unit tests to the existing script. --- .ci.yaml | 84 +++++++++++++++++++ .ci/scripts/build_all_plugins.sh | 8 ++ .ci/scripts/build_examples_uwp.sh | 7 ++ .ci/scripts/create_all_plugins_app.sh | 7 ++ .ci/scripts/native_test_win32.sh | 7 ++ .ci/scripts/plugin_tools_tests.sh | 7 ++ .ci/targets/build_all_plugins.yaml | 7 ++ .ci/targets/plugin_tools_tests.yaml | 5 ++ .ci/targets/uwp_build_and_platform_tests.yaml | 5 ++ .../windows_build_and_platform_tests.yaml | 9 +- .cirrus.yml | 2 +- 11 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 .ci/scripts/build_all_plugins.sh create mode 100644 .ci/scripts/build_examples_uwp.sh create mode 100644 .ci/scripts/create_all_plugins_app.sh create mode 100644 .ci/scripts/native_test_win32.sh create mode 100644 .ci/scripts/plugin_tools_tests.sh create mode 100644 .ci/targets/build_all_plugins.yaml create mode 100644 .ci/targets/plugin_tools_tests.yaml create mode 100644 .ci/targets/uwp_build_and_platform_tests.yaml diff --git a/.ci.yaml b/.ci.yaml index ebedd203b3ca..a63f149403d9 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -35,6 +35,7 @@ platform_properties: os: Windows targets: + # TODO(stuartmorgan) Remove once the renamed version below has propagated. - name: Windows Plugins master channel recipe: plugins/plugins timeout: 30 @@ -47,6 +48,7 @@ targets: ] scheduler: luci + # TODO(stuartmorgan) Remove once the renamed version below has propagated. - name: Windows Plugins stable channel recipe: plugins/plugins timeout: 30 @@ -60,6 +62,88 @@ targets: ] scheduler: luci + - name: Windows win32_build+platform-tests master + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: windows_build_and_platform_tests.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows win32_build+platform-tests stable + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: uwp_build_and_platform_tests.yaml + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows build_all_plugins master + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: build_all_plugins.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows build_all_plugins stable + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: build_all_plugins.yaml + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows uwp-build+platform-tests master + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: uwp_build_and_platform_tests.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows plugin_tools_tests + recipe: plugins/plugins + bringup: true + timeout: 30 + properties: + # TODO(stuartmorgan): Uncomment when removing bringup. + #add_recipes_cq: "true" + target_file: plugin_tools_tests.yaml + scheduler: luci + - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml timeout: 30 diff --git a/.ci/scripts/build_all_plugins.sh b/.ci/scripts/build_all_plugins.sh new file mode 100644 index 000000000000..008dea7c5e13 --- /dev/null +++ b/.ci/scripts/build_all_plugins.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +cd all_plugins +flutter build windows --debug +flutter build windows --release diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh new file mode 100644 index 000000000000..639cb054e4b7 --- /dev/null +++ b/.ci/scripts/build_examples_uwp.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --winuwp \ + --packages-for-branch diff --git a/.ci/scripts/create_all_plugins_app.sh b/.ci/scripts/create_all_plugins_app.sh new file mode 100644 index 000000000000..196fef9b06c9 --- /dev/null +++ b/.ci/scripts/create_all_plugins_app.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart all-plugins-app \ + --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh new file mode 100644 index 000000000000..938515784412 --- /dev/null +++ b/.ci/scripts/native_test_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ + --no-integration --packages-for-branch diff --git a/.ci/scripts/plugin_tools_tests.sh b/.ci/scripts/plugin_tools_tests.sh new file mode 100644 index 000000000000..96eec4349f08 --- /dev/null +++ b/.ci/scripts/plugin_tools_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +cd script/tool +dart pub run test diff --git a/.ci/targets/build_all_plugins.yaml b/.ci/targets/build_all_plugins.yaml new file mode 100644 index 000000000000..b51a5b18dfd9 --- /dev/null +++ b/.ci/targets/build_all_plugins.yaml @@ -0,0 +1,7 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_plugins app + script: .ci/scripts/create_all_plugins_app.sh + - name: build all_plugins + script: .ci/scripts/build_all_plugins.sh diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml new file mode 100644 index 000000000000..265e74bdd06b --- /dev/null +++ b/.ci/targets/plugin_tools_tests.yaml @@ -0,0 +1,5 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: tool unit tests + script: .ci/scripts/plugin_tools_tests.sh diff --git a/.ci/targets/uwp_build_and_platform_tests.yaml b/.ci/targets/uwp_build_and_platform_tests.yaml new file mode 100644 index 000000000000..a7f070776ff1 --- /dev/null +++ b/.ci/targets/uwp_build_and_platform_tests.yaml @@ -0,0 +1,5 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: build examples (UWP) + script: .ci/scripts/build_examples_uwp.sh diff --git a/.ci/targets/windows_build_and_platform_tests.yaml b/.ci/targets/windows_build_and_platform_tests.yaml index cba120073310..cda3e57f75d2 100644 --- a/.ci/targets/windows_build_and_platform_tests.yaml +++ b/.ci/targets/windows_build_and_platform_tests.yaml @@ -1,8 +1,9 @@ - tasks: - - name: "prepare tool" + - name: prepare tool script: .ci/scripts/prepare_tool.sh - - name: "build examples" + - name: build examples (Win32) script: .ci/scripts/build_examples_win32.sh - - name: "drive examples" + - name: native unit tests (Win32) + script: .ci/scripts/native_test_win32.sh + - name: drive examples (Win32) script: .ci/scripts/drive_examples_win32.sh diff --git a/.cirrus.yml b/.cirrus.yml index 10d668d8d1d7..56f312dea929 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -66,7 +66,7 @@ task: - name: plugin_tools_tests script: - cd script/tool - - CIRRUS_BUILD_ID=null pub run test + - dart pub run test - name: publishable version_check_script: ./script/tool_runner.sh version-check publish_check_script: ./script/tool_runner.sh publish-check From 2e84965c3132a92b219f870ead2f5514931e24e2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 8 Sep 2021 12:31:16 -0400 Subject: [PATCH 014/259] [ci] Fix and standardize target/task names (#4323) Fixes the target names in LUCI to not use '+', since it's not supported. To better align the names between the two different infrastructures, also updates the task naming in cirrus.yml to not use +. It also standardizes the naming form across both systems as: ` - ` where: - `` is always there on LUCI, where required, but only added where it's potentially ambiguous on Cirrus. - `' is omitted when the test is not target-platform specific. - `' uses underscores, which is consistent with flutter/flutter (and with Cirrus step naming). - `` is only explicitly set (to the channel) on LUCI; Cirrus automatically adds channel info there due to the way `matrix` works. --- .ci.yaml | 12 ++++++------ .cirrus.yml | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index a63f149403d9..cf0fdc807a01 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -62,7 +62,7 @@ targets: ] scheduler: luci - - name: Windows win32_build+platform-tests master + - name: Windows win32-platform_tests master recipe: plugins/plugins bringup: true timeout: 30 @@ -76,14 +76,14 @@ targets: ] scheduler: luci - - name: Windows win32_build+platform-tests stable + - name: Windows win32-platform_tests stable recipe: plugins/plugins bringup: true timeout: 30 properties: # TODO(stuartmorgan): Uncomment when removing bringup. #add_recipes_cq: "true" - target_file: uwp_build_and_platform_tests.yaml + target_file: windows_build_and_platform_tests.yaml channel: stable dependencies: > [ @@ -91,7 +91,7 @@ targets: ] scheduler: luci - - name: Windows build_all_plugins master + - name: Windows windows-build_all_plugins master recipe: plugins/plugins bringup: true timeout: 30 @@ -105,7 +105,7 @@ targets: ] scheduler: luci - - name: Windows build_all_plugins stable + - name: Windows windows-build_all_plugins stable recipe: plugins/plugins bringup: true timeout: 30 @@ -120,7 +120,7 @@ targets: ] scheduler: luci - - name: Windows uwp-build+platform-tests master + - name: Windows uwp-platform_tests master recipe: plugins/plugins bringup: true timeout: 30 diff --git a/.cirrus.yml b/.cirrus.yml index 56f312dea929..9ca3a875c01e 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -63,7 +63,7 @@ task: namespace: default matrix: ### Platform-agnostic tasks ### - - name: plugin_tools_tests + - name: Linux plugin_tools_tests script: - cd script/tool - dart pub run test @@ -74,7 +74,7 @@ task: format_script: ./script/tool_runner.sh format --fail-on-change pubspec_script: ./script/tool_runner.sh pubspec-check license_script: dart $PLUGIN_TOOL license-check - - name: test + - name: dart_unit_tests env: matrix: CHANNEL: "master" @@ -94,7 +94,7 @@ task: # See the comment in script/configs/custom_analysis.yaml for details. - ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml ### Android tasks ### - - name: build_all_plugins_apk + - name: android-build_all_plugins env: BUILD_ALL_ARGS: "apk" matrix: @@ -102,7 +102,7 @@ task: CHANNEL: "stable" << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - - name: build_all_plugins_web + - name: web-build_all_plugins env: BUILD_ALL_ARGS: "web" matrix: @@ -110,7 +110,7 @@ task: CHANNEL: "stable" << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Linux desktop tasks ### - - name: build_all_plugins_linux + - name: linux-build_all_plugins env: BUILD_ALL_ARGS: "linux" matrix: @@ -119,7 +119,7 @@ task: setup_script: - flutter config --enable-linux-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: linux-build+platform-tests + - name: linux-platform_tests env: matrix: CHANNEL: "master" @@ -148,7 +148,7 @@ task: memory: 12G matrix: ### Android tasks ### - - name: android-build+platform-tests + - name: android-platform_tests env: matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" @@ -202,7 +202,7 @@ task: type: text/xml format: android-lint ### Web tasks ### - - name: web-build+platform-tests + - name: web-platform_tests env: matrix: CHANNEL: "master" @@ -224,18 +224,18 @@ task: << : *FLUTTER_UPGRADE_TEMPLATE matrix: ### iOS+macOS tasks *** - - name: lint_darwin_plugins + - name: darwin-lint_podspecs script: - ./script/tool_runner.sh podspecs ### iOS tasks ### - - name: build_all_plugins_ipa + - name: ios-build_all_plugins env: BUILD_ALL_ARGS: "ios --no-codesign" matrix: CHANNEL: "master" CHANNEL: "stable" << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: ios-build+platform-tests + - name: ios-platform_tests env: PATH: $PATH:/usr/local/bin matrix: @@ -262,7 +262,7 @@ task: # So we run `drive-examples` after `native-test`; changing the order will result ci failure. - ./script/tool_runner.sh drive-examples --ios --exclude=script/configs/exclude_integration_ios.yaml ### macOS desktop tasks ### - - name: build_all_plugins_macos + - name: macos-build_all_plugins env: BUILD_ALL_ARGS: "macos" matrix: @@ -271,7 +271,7 @@ task: setup_script: - flutter config --enable-macos-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: macos-build+platform-tests + - name: macos-platform_tests env: matrix: CHANNEL: "master" From f988f6191a2ddb162c962ea5ac00cda4a00d9783 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Wed, 8 Sep 2021 14:47:04 -0700 Subject: [PATCH 015/259] [ci] Update lewagon/wait-on-check-action to latest version. (#4326) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2393eadab4c5..9fbe499f0963 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe + uses: lewagon/wait-on-check-action@5e937358caba2c7876a2ee06e4a48d0664fe4967 with: ref: ${{ github.sha }} running-workflow-name: 'release' From f460b7967f4b1aa491c1be8cc9f4ad8c9744b067 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 9 Sep 2021 09:59:18 -0400 Subject: [PATCH 016/259] [ci] Enable the new Windows targets (#4325) Now that the builders have propagated, enable all the new tests and remove the obsolete versions. --- .ci.yaml | 51 +++---------------- .../test/drive_examples_command_test.dart | 4 +- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index cf0fdc807a01..00aecdca1122 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -35,40 +35,11 @@ platform_properties: os: Windows targets: - # TODO(stuartmorgan) Remove once the renamed version below has propagated. - - name: Windows Plugins master channel - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: windows_build_and_platform_tests.yaml - dependencies: > - [ - {"dependency": "vs_build"} - ] - scheduler: luci - - # TODO(stuartmorgan) Remove once the renamed version below has propagated. - - name: Windows Plugins stable channel - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: windows_build_and_platform_tests.yaml - channel: stable - dependencies: > - [ - {"dependency": "vs_build"} - ] - scheduler: luci - - name: Windows win32-platform_tests master recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml dependencies: > [ @@ -78,11 +49,9 @@ targets: - name: Windows win32-platform_tests stable recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml channel: stable dependencies: > @@ -93,11 +62,9 @@ targets: - name: Windows windows-build_all_plugins master recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: build_all_plugins.yaml dependencies: > [ @@ -107,11 +74,9 @@ targets: - name: Windows windows-build_all_plugins stable recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: build_all_plugins.yaml channel: stable dependencies: > @@ -122,11 +87,9 @@ targets: - name: Windows uwp-platform_tests master recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: uwp_build_and_platform_tests.yaml dependencies: > [ @@ -136,11 +99,9 @@ targets: - name: Windows plugin_tools_tests recipe: plugins/plugins - bringup: true timeout: 30 properties: - # TODO(stuartmorgan): Uncomment when removing bringup. - #add_recipes_cq: "true" + add_recipes_cq: "true" target_file: plugin_tools_tests.yaml scheduler: luci diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 85d2326d0689..a7a1652c2fc2 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:io' as io; import 'package:args/command_runner.dart'; @@ -60,7 +61,8 @@ void main() { final String output = '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess(stdout: output); + final MockProcess mockDevicesProcess = + MockProcess(stdout: output, stdoutEncoding: utf8); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [mockDevicesProcess]; From 5266acb44c868b4df34493b7d863c0bfc9e2a798 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 9 Sep 2021 09:37:06 -0700 Subject: [PATCH 017/259] Ignore unnecessary_import in legacy analysis options (#4129) --- analysis_options_legacy.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/analysis_options_legacy.yaml b/analysis_options_legacy.yaml index 2b62a6a9e2b9..793640e22d27 100644 --- a/analysis_options_legacy.yaml +++ b/analysis_options_legacy.yaml @@ -7,6 +7,9 @@ analyzer: - '**/*.mocks.dart' # Mockito @GenerateMocks errors: always_require_non_null_named_parameters: false # not needed with nnbd + # TODO(https://github.com/flutter/flutter/issues/74381): + # Clean up existing unnecessary imports, and remove line to ignore. + unnecessary_import: ignore unnecessary_null_comparison: false # Turned as long as nnbd mix-mode is supported. linter: rules: From 3fd82b9208496317108e7d4a25df62dd0520b1f3 Mon Sep 17 00:00:00 2001 From: byunme Date: Thu, 9 Sep 2021 13:02:06 -0700 Subject: [PATCH 018/259] [video_player] interface: add support for content-uri based videos (android only) (#4307) --- .../video_player_platform_interface/CHANGELOG.md | 6 +++++- .../lib/method_channel_video_player.dart | 3 +++ .../lib/video_player_platform_interface.dart | 3 +++ .../video_player_platform_interface/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 24631513f800..b3da9c8924ef 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.0 + +* Add `contentUri` to `DataSourceType`. + ## 4.1.0 * Add `httpHeaders` to `DataSource` @@ -29,7 +33,7 @@ ## 2.1.0 -* Add VideoPlayerOptions with audo mix mode +* Add VideoPlayerOptions with audio mix mode ## 2.0.2 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 e92e87013d68..e01e5b8c072c 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -42,6 +42,9 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { case DataSourceType.file: message.uri = dataSource.uri; break; + case DataSourceType.contentUri: + message.uri = dataSource.uri; + break; } TextureMessage response = await _api.create(message); 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 b2bff990401e..21ad972d8e06 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -196,6 +196,9 @@ enum DataSourceType { /// The video was loaded off of the local filesystem. file, + + /// The video is available via contentUri. Android only. + contentUri, } /// The file format of the given video. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 2a0ef10a9d2b..35b30793a20f 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/video_player 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: 4.1.0 +version: 4.2.0 environment: sdk: ">=2.12.0 <3.0.0" From 1f472becf369e247dfd511ef0aa95c38ba75b0f1 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 9 Sep 2021 14:07:07 -0700 Subject: [PATCH 019/259] [camera] Bump minimum Flutter version and iOS deployment target (#4327) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/README.md | 6 ++++-- .../camera/example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../camera/example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- packages/camera/camera/example/pubspec.yaml | 4 ++-- packages/camera/camera/ios/camera.podspec | 2 +- packages/camera/camera/pubspec.yaml | 6 +++--- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index b141fab62595..ba2cb313b908 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.3 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 0.9.2+2 * Ensure that setting the exposure offset returns the new offset value on Android. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index c66ed67af6cb..aa34273fb92c 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -6,7 +6,7 @@ A Flutter plugin for iOS and Android allowing access to the device cameras. *Note*: This plugin is still under development, and some APIs might not be available yet. We are working on a refactor which can be followed here: [issue](https://github.com/flutter/flutter/issues/31225) -## Features: +## Features * Display live camera preview in a widget. * Snapshots can be captured and saved to a file. @@ -19,7 +19,9 @@ First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter. ### iOS -iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info_plus](https://pub.dev/packages/device_info_plus) plugin. +The camera plugin functionality works on iOS 10.0 or higher. If compiling for any version lower than 10.0, +make sure to programmatically check the version of iOS running on the device before using any camera plugin features. +The [device_info_plus](https://pub.dev/packages/device_info_plus) plugin, for example, can be used to check the iOS version. Add two rows to the `ios/Runner/Info.plist`: diff --git a/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 5a622f17fc63..8520bb00fb2f 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -504,7 +504,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -554,7 +554,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index eb8995e2f354..1899835aca50 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.22.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: camera: diff --git a/packages/camera/camera/ios/camera.podspec b/packages/camera/camera/ios/camera.podspec index 4f9955311fb9..4a142bd4589a 100644 --- a/packages/camera/camera/ios/camera.podspec +++ b/packages/camera/camera/ios/camera.podspec @@ -17,6 +17,6 @@ A Flutter plugin to use the camera from your Flutter app. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } end diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 582a830ebb4c..8d578bbdf065 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,11 +4,11 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.2+2 +version: 0.9.3 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 334d8e2e8db13f5d304f8a738f77809d86f27159 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 9 Sep 2021 23:27:05 +0200 Subject: [PATCH 020/259] [webview_flutter] Implementation of the webview_flutter_platform_interface package (#4302) --- .../AUTHORS | 67 +++ .../CHANGELOG.md | 3 + .../LICENSE | 25 + .../README.md | 23 + .../webview_method_channel.dart | 223 +++++++++ .../javascript_channel_registry.dart | 42 ++ .../platform_interface.dart | 8 + .../platform_interface/webview_platform.dart | 66 +++ .../webview_platform_callbacks_handler.dart | 32 ++ .../webview_platform_controller.dart | 177 +++++++ .../src/types/auto_media_playback_policy.dart | 22 + .../lib/src/types/creation_params.dart | 60 +++ .../lib/src/types/javascript_channel.dart | 39 ++ .../lib/src/types/javascript_message.dart | 14 + .../lib/src/types/javascript_mode.dart | 12 + .../lib/src/types/types.dart | 12 + .../lib/src/types/web_resource_error.dart | 57 +++ .../src/types/web_resource_error_type.dart | 66 +++ .../lib/src/types/web_settings.dart | 123 +++++ .../webview_flutter_platform_interface.dart | 7 + .../pubspec.yaml | 22 + .../webview_method_channel_test.dart | 457 ++++++++++++++++++ .../javascript_channel_registry_test.dart | 119 +++++ .../src/types/javascript_channel_test.dart | 48 ++ 24 files changed, 1724 insertions(+) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/AUTHORS create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/LICENSE create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/README.md create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..9e217a04e961 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Extracted platform interface from `webview_flutter`. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/LICENSE b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/README.md b/packages/webview_flutter/webview_flutter_platform_interface/README.md new file mode 100644 index 000000000000..31e57ab61597 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/README.md @@ -0,0 +1,23 @@ +# webview_flutter_platform_interface + +A common platform interface for the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin. + +This interface allows platform-specific implementations of the `webview_flutter` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `webview_flutter`, extend +[`WebviewPlatform`](lib/src/platform_interface/webview_platform.dart) with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`WebviewPlatform` by calling +`WebviewPlatform.setInstance(MyPlatformWebview())`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart new file mode 100644 index 000000000000..b467daf72a08 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -0,0 +1,223 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import '../platform_interface/javascript_channel_registry.dart'; +import '../platform_interface/platform_interface.dart'; +import '../types/types.dart'; + +/// A [WebViewPlatformController] that uses a method channel to control the webview. +class MethodChannelWebViewPlatform implements WebViewPlatformController { + /// Constructs an instance that will listen for webviews broadcasting to the + /// given [id], using the given [WebViewPlatformCallbacksHandler]. + MethodChannelWebViewPlatform( + int id, + this._platformCallbacksHandler, + this._javascriptChannelRegistry, + ) : assert(_platformCallbacksHandler != null), + _channel = MethodChannel('plugins.flutter.io/webview_$id') { + _channel.setMethodCallHandler(_onMethodCall); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformCallbacksHandler _platformCallbacksHandler; + + final MethodChannel _channel; + + static const MethodChannel _cookieManagerChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + Future _onMethodCall(MethodCall call) async { + switch (call.method) { + case 'javascriptChannelMessage': + final String channel = call.arguments['channel']!; + final String message = call.arguments['message']!; + _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message); + return true; + case 'navigationRequest': + return await _platformCallbacksHandler.onNavigationRequest( + url: call.arguments['url']!, + isForMainFrame: call.arguments['isForMainFrame']!, + ); + case 'onPageFinished': + _platformCallbacksHandler.onPageFinished(call.arguments['url']!); + return null; + case 'onProgress': + _platformCallbacksHandler.onProgress(call.arguments['progress']); + return null; + case 'onPageStarted': + _platformCallbacksHandler.onPageStarted(call.arguments['url']!); + return null; + case 'onWebResourceError': + _platformCallbacksHandler.onWebResourceError( + WebResourceError( + errorCode: call.arguments['errorCode']!, + description: call.arguments['description']!, + // iOS doesn't support `failingUrl`. + failingUrl: call.arguments['failingUrl'], + domain: call.arguments['domain'], + errorType: call.arguments['errorType'] == null + ? null + : WebResourceErrorType.values.firstWhere( + (WebResourceErrorType type) { + return type.toString() == + '$WebResourceErrorType.${call.arguments['errorType']}'; + }, + ), + ), + ); + return null; + } + + throw MissingPluginException( + '${call.method} was invoked but has no handler', + ); + } + + @override + Future loadUrl( + String url, + Map? headers, + ) async { + assert(url != null); + return _channel.invokeMethod('loadUrl', { + 'url': url, + 'headers': headers, + }); + } + + @override + Future currentUrl() => _channel.invokeMethod('currentUrl'); + + @override + Future canGoBack() => + _channel.invokeMethod("canGoBack").then((result) => result!); + + @override + Future canGoForward() => + _channel.invokeMethod("canGoForward").then((result) => result!); + + @override + Future goBack() => _channel.invokeMethod("goBack"); + + @override + Future goForward() => _channel.invokeMethod("goForward"); + + @override + Future reload() => _channel.invokeMethod("reload"); + + @override + Future clearCache() => _channel.invokeMethod("clearCache"); + + @override + Future updateSettings(WebSettings settings) async { + final Map updatesMap = _webSettingsToMap(settings); + if (updatesMap.isNotEmpty) { + await _channel.invokeMethod('updateSettings', updatesMap); + } + } + + @override + Future evaluateJavascript(String javascriptString) { + return _channel + .invokeMethod('evaluateJavascript', javascriptString) + .then((result) => result!); + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'addJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'removeJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future getTitle() => _channel.invokeMethod("getTitle"); + + @override + Future scrollTo(int x, int y) { + return _channel.invokeMethod('scrollTo', { + 'x': x, + 'y': y, + }); + } + + @override + Future scrollBy(int x, int y) { + return _channel.invokeMethod('scrollBy', { + 'x': x, + 'y': y, + }); + } + + @override + Future getScrollX() => + _channel.invokeMethod("getScrollX").then((result) => result!); + + @override + Future getScrollY() => + _channel.invokeMethod("getScrollY").then((result) => result!); + + /// Method channel implementation for [WebViewPlatform.clearCookies]. + static Future clearCookies() { + return _cookieManagerChannel + .invokeMethod('clearCookies') + .then((dynamic result) => result!); + } + + static Map _webSettingsToMap(WebSettings? settings) { + final Map map = {}; + void _addIfNonNull(String key, dynamic value) { + if (value == null) { + return; + } + map[key] = value; + } + + void _addSettingIfPresent(String key, WebSetting setting) { + if (!setting.isPresent) { + return; + } + map[key] = setting.value; + } + + _addIfNonNull('jsMode', settings!.javascriptMode?.index); + _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); + _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); + _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addIfNonNull( + 'gestureNavigationEnabled', settings.gestureNavigationEnabled); + _addIfNonNull( + 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); + _addSettingIfPresent('userAgent', settings.userAgent); + return map; + } + + /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. + /// + /// This is used for the `creationParams` argument of the platform views created by + /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. + static Map creationParamsToMap( + CreationParams creationParams, { + bool usesHybridComposition = false, + }) { + return { + 'initialUrl': creationParams.initialUrl, + 'settings': _webSettingsToMap(creationParams.webSettings), + 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), + 'userAgent': creationParams.userAgent, + 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, + 'usesHybridComposition': usesHybridComposition, + }; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart new file mode 100644 index 000000000000..142d8eb00950 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../types/javascript_channel.dart'; +import '../types/javascript_message.dart'; + +/// Utility class for managing named JavaScript channels and forwarding incoming +/// messages on the correct channel. +class JavascriptChannelRegistry { + /// Constructs a [JavascriptChannelRegistry] initializing it with the given + /// set of [JavascriptChannel]s. + JavascriptChannelRegistry(Set? channels) { + updateJavascriptChannelsFromSet(channels); + } + + /// Maps a channel name to a channel. + final Map channels = {}; + + /// Invoked when a JavaScript channel message is received. + void onJavascriptChannelMessage(String channel, String message) { + final JavascriptChannel? javascriptChannel = channels[channel]; + + if (javascriptChannel == null) { + throw ArgumentError('No channel registered with name $channel.'); + } + + javascriptChannel.onMessageReceived(JavascriptMessage(message)); + } + + /// Updates the set of [JavascriptChannel]s with the new set. + void updateJavascriptChannelsFromSet(Set? channels) { + this.channels.clear(); + if (channels == null) { + return; + } + + for (final JavascriptChannel channel in channels) { + this.channels[channel.name] = channel; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart new file mode 100644 index 000000000000..43f967fb13b0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'javascript_channel_registry.dart'; +export 'webview_platform.dart'; +export 'webview_platform_callbacks_handler.dart'; +export 'webview_platform_controller.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart new file mode 100644 index 000000000000..4732f54d6456 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; + +import '../types/types.dart'; +import 'webview_platform_callbacks_handler.dart'; +import 'webview_platform_controller.dart'; + +/// Signature for callbacks reporting that a [WebViewPlatformController] was created. +/// +/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. +typedef WebViewPlatformCreatedCallback = void Function( + WebViewPlatformController? webViewPlatformController); + +/// Interface for a platform implementation of a WebView. +/// +/// [WebView.platform] controls the builder that is used by [WebView]. +/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations +/// for Android and iOS respectively. +abstract class WebViewPlatform { + /// Builds a new WebView. + /// + /// Returns a Widget tree that embeds the created webview. + /// + /// `creationParams` are the initial parameters used to setup the webview. + /// + /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created + /// [WebViewPlatformController]. + /// + /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] + /// implementation is created with the [WebViewPlatformController] instance as a parameter. + /// + /// `gestureRecognizers` specifies which gestures should be consumed by the web view. + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + /// + /// `webViewPlatformHandler` must not be null. + Widget build({ + required BuildContext context, + // TODO(amirh): convert this to be the actual parameters. + // I'm starting without it as the PR is starting to become pretty big. + // I'll followup with the conversion PR. + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }); + + /// Clears all cookies for all [WebView] instances. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() { + throw UnimplementedError( + "WebView clearCookies is not implemented on the current platform"); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart new file mode 100644 index 000000000000..44dae2ece434 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../types/types.dart'; + +/// Interface for callbacks made by [WebViewPlatformController]. +/// +/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. +/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. +abstract class WebViewPlatformCallbacksHandler { + /// Invoked by [WebViewPlatformController] when a navigation request is pending. + /// + /// If true is returned the navigation is allowed, otherwise it is blocked. + FutureOr onNavigationRequest( + {required String url, required bool isForMainFrame}); + + /// Invoked by [WebViewPlatformController] when a page has started loading. + void onPageStarted(String url); + + /// Invoked by [WebViewPlatformController] when a page has finished loading. + void onPageFinished(String url); + + /// Invoked by [WebViewPlatformController] when a page is loading. + /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. + void onProgress(int progress); + + /// Report web resource loading error to the host application. + void onWebResourceError(WebResourceError error); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart new file mode 100644 index 000000000000..319ca7e7a845 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -0,0 +1,177 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../types/types.dart'; +import 'webview_platform_callbacks_handler.dart'; + +/// Interface for talking to the webview's platform implementation. +/// +/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is +/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. +/// +/// Platform implementations that live in a separate package should extend this class rather than +/// implement it as webview_flutter does not consider newly added methods to be breaking changes. +/// Extending this class (using `extends`) ensures that the subclass will get the default +/// implementation, while platform implementations that `implements` this interface will be broken +/// by newly added [WebViewPlatformController] methods. +abstract class WebViewPlatformController { + /// Creates a new WebViewPlatform. + /// + /// Callbacks made by the WebView will be delegated to `handler`. + /// + /// The `handler` parameter must not be null. + WebViewPlatformController(WebViewPlatformCallbacksHandler handler); + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, + Map? headers, + ) { + throw UnimplementedError( + "WebView loadUrl is not implemented on the current platform"); + } + + /// Updates the webview settings. + /// + /// Any non null field in `settings` will be set as the new setting value. + /// All null fields in `settings` are ignored. + Future updateSettings(WebSettings setting) { + throw UnimplementedError( + "WebView updateSettings is not implemented on the current platform"); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If no URL was ever loaded, returns `null`. + Future currentUrl() { + throw UnimplementedError( + "WebView currentUrl is not implemented on the current platform"); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + throw UnimplementedError( + "WebView canGoBack is not implemented on the current platform"); + } + + /// Checks whether there's a forward history item. + Future canGoForward() { + throw UnimplementedError( + "WebView canGoForward is not implemented on the current platform"); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + throw UnimplementedError( + "WebView goBack is not implemented on the current platform"); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + throw UnimplementedError( + "WebView goForward is not implemented on the current platform"); + } + + /// Reloads the current URL. + Future reload() { + throw UnimplementedError( + "WebView reload is not implemented on the current platform"); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + Future clearCache() { + throw UnimplementedError( + "WebView clearCache is not implemented on the current platform"); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the type of the + /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). + Future evaluateJavascript(String javascriptString) { + throw UnimplementedError( + "WebView evaluateJavascript is not implemented on the current platform"); + } + + /// Adds new JavaScript channels to the set of enabled channels. + /// + /// For each value in this list the platform's webview should make sure that a corresponding + /// property with a postMessage method is set on `window`. For example for a JavaScript channel + /// named `Foo` it should be possible for JavaScript code executing in the webview to do + /// + /// ```javascript + /// Foo.postMessage('hello'); + /// ``` + /// + /// See also: [CreationParams.javascriptChannelNames]. + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView addJavascriptChannels is not implemented on the current platform"); + } + + /// Removes JavaScript channel names from the set of enabled channels. + /// + /// This disables channels that were previously enabled by [addJavaScriptChannels] or through + /// [CreationParams.javascriptChannelNames]. + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView removeJavascriptChannels is not implemented on the current platform"); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + throw UnimplementedError( + "WebView getTitle is not implemented on the current platform"); + } + + /// Set the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. + Future scrollTo(int x, int y) { + throw UnimplementedError( + "WebView scrollTo is not implemented on the current platform"); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. + Future scrollBy(int x, int y) { + throw UnimplementedError( + "WebView scrollBy is not implemented on the current platform"); + } + + /// Return the horizontal scroll position of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + throw UnimplementedError( + "WebView getScrollX is not implemented on the current platform"); + } + + /// Return the vertical scroll position of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + throw UnimplementedError( + "WebView getScrollY is not implemented on the current platform"); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart new file mode 100644 index 000000000000..7d6927ac7957 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Specifies possible restrictions on automatic media playback. +/// +/// This is typically used in [WebView.initialMediaPlaybackPolicy]. +// The method channel implementation is marshalling this enum to the value's index, so the order +// is important. +enum AutoMediaPlaybackPolicy { + /// Starting any kind of media playback requires a user action. + /// + /// For example: JavaScript code cannot start playing media unless the code was executed + /// as a result of a user action (like a touch event). + require_user_action_for_all_media_types, + + /// Starting any kind of media playback is always allowed. + /// + /// For example: JavaScript code that's triggered when the page is loaded can start playing + /// video or audio. + always_allow, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart new file mode 100644 index 000000000000..f213e976ad84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'auto_media_playback_policy.dart'; +import 'web_settings.dart'; + +/// Configuration to use when creating a new [WebViewPlatformController]. +/// +/// The `autoMediaPlaybackPolicy` parameter must not be null. +class CreationParams { + /// Constructs an instance to use when creating a new + /// [WebViewPlatformController]. + /// + /// The `autoMediaPlaybackPolicy` parameter must not be null. + CreationParams({ + this.initialUrl, + this.webSettings, + this.javascriptChannelNames = const {}, + this.userAgent, + this.autoMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + }) : assert(autoMediaPlaybackPolicy != null); + + /// The initialUrl to load in the webview. + /// + /// When null the webview will be created without loading any page. + final String? initialUrl; + + /// The initial [WebSettings] for the new webview. + /// + /// This can later be updated with [WebViewPlatformController.updateSettings]. + final WebSettings? webSettings; + + /// The initial set of JavaScript channels that are configured for this webview. + /// + /// For each value in this set the platform's webview should make sure that a corresponding + /// property with a postMessage method is set on `window`. For example for a JavaScript channel + /// named `Foo` it should be possible for JavaScript code executing in the webview to do + /// + /// ```javascript + /// Foo.postMessage('hello'); + /// ``` + // TODO(amirh): describe what should happen when postMessage is called once that code is migrated + // to PlatformWebView. + final Set javascriptChannelNames; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; + + @override + String toString() { + return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart new file mode 100644 index 000000000000..8b31f5b6061e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'javascript_message.dart'; + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); + +final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); + +/// A named channel for receiving messaged from JavaScript code running inside a web view. +class JavascriptChannel { + /// Constructs a Javascript channel. + /// + /// The parameters `name` and `onMessageReceived` must not be null. + JavascriptChannel({ + required this.name, + required this.onMessageReceived, + }) : assert(name != null), + assert(onMessageReceived != null), + assert(_validChannelNames.hasMatch(name)); + + /// The channel's name. + /// + /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to + /// the Javascript window object's property named `name`. + /// + /// The name must start with a letter or underscore(_), followed by any combination of those + /// characters plus digits. + /// + /// Note that any JavaScript existing `window` property with this name will be overriden. + /// + /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. + final String name; + + /// A callback that's invoked when a message is received through the channel. + final JavascriptMessageHandler onMessageReceived; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart new file mode 100644 index 000000000000..8d080452c54a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A message that was sent by JavaScript code running in a [WebView]. +class JavascriptMessage { + /// Constructs a JavaScript message object. + /// + /// The `message` parameter must not be null. + const JavascriptMessage(this.message) : assert(message != null); + + /// The contents of the message that was sent by the JavaScript code. + final String message; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart new file mode 100644 index 000000000000..53d049175907 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Describes the state of JavaScript support in a given web view. +enum JavascriptMode { + /// JavaScript execution is disabled. + disabled, + + /// JavaScript execution is not restricted. + unrestricted, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..b1a9b9b9daa8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'auto_media_playback_policy.dart'; +export 'creation_params.dart'; +export 'javascript_channel.dart'; +export 'javascript_message.dart'; +export 'javascript_mode.dart'; +export 'web_resource_error.dart'; +export 'web_resource_error_type.dart'; +export 'web_settings.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart new file mode 100644 index 000000000000..b61671f0ac45 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'web_resource_error_type.dart'; + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +class WebResourceError { + /// Creates a new [WebResourceError] + /// + /// A user should not need to instantiate this class, but will receive one in + /// [WebResourceErrorCallback]. + WebResourceError({ + required this.errorCode, + required this.description, + this.domain, + this.errorType, + this.failingUrl, + }) : assert(errorCode != null), + assert(description != null); + + /// Raw code of the error from the respective platform. + /// + /// On Android, the error code will be a constant from a + /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and + /// will have a corresponding [errorType]. + /// + /// On iOS, the error code will be a constant from `NSError.code` in + /// Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. Some possible error codes + /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. + final int errorCode; + + /// The domain of where to find the error code. + /// + /// This field is only available on iOS and represents a "domain" from where + /// the [errorCode] is from. This value is taken directly from an `NSError` + /// in Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. + final String? domain; + + /// Description of the error that can be used to communicate the problem to the user. + final String description; + + /// The type this error can be categorized as. + /// + /// This will never be `null` on Android, but can be `null` on iOS. + final WebResourceErrorType? errorType; + + /// Gets the URL for which the resource request was made. + /// + /// This value is not provided on iOS. Alternatively, you can keep track of + /// the last values provided to [WebViewPlatformController.loadUrl]. + final String? failingUrl; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart new file mode 100644 index 000000000000..a45816df8323 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Possible error type categorizations used by [WebResourceError]. +enum WebResourceErrorType { + /// User authentication failed on server. + authentication, + + /// Malformed URL. + badUrl, + + /// Failed to connect to the server. + connect, + + /// Failed to perform SSL handshake. + failedSslHandshake, + + /// Generic file error. + file, + + /// File not found. + fileNotFound, + + /// Server or proxy hostname lookup failed. + hostLookup, + + /// Failed to read or write to the server. + io, + + /// User authentication failed on proxy. + proxyAuthentication, + + /// Too many redirects. + redirectLoop, + + /// Connection timed out. + timeout, + + /// Too many requests during this load. + tooManyRequests, + + /// Generic error. + unknown, + + /// Resource load was canceled by Safe Browsing. + unsafeResource, + + /// Unsupported authentication scheme (not basic or digest). + unsupportedAuthScheme, + + /// Unsupported URI scheme. + unsupportedScheme, + + /// The web content process was terminated. + webContentProcessTerminated, + + /// The web view was invalidated. + webViewInvalidated, + + /// A JavaScript exception occurred. + javaScriptExceptionOccurred, + + /// The result of JavaScript execution could not be returned. + javaScriptResultTypeIsUnsupported, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart new file mode 100644 index 000000000000..48b2de9c1ca0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart @@ -0,0 +1,123 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +import 'javascript_mode.dart'; + +/// A single setting for configuring a WebViewPlatform which may be absent. +class WebSetting { + /// Constructs an absent setting instance. + /// + /// The [isPresent] field for the instance will be false. + /// + /// Accessing [value] for an absent instance will throw. + WebSetting.absent() + : _value = null, + isPresent = false; + + /// Constructs a setting of the given `value`. + /// + /// The [isPresent] field for the instance will be true. + WebSetting.of(T value) + : _value = value, + isPresent = true; + + final T? _value; + + /// The setting's value. + /// + /// Throws if [WebSetting.isPresent] is false. + T get value { + if (!isPresent) { + throw StateError('Cannot access a value of an absent WebSetting'); + } + assert(isPresent); + // The intention of this getter is to return T whether it is nullable or + // not whereas _value is of type T? since _value can be null even when + // T is not nullable (when isPresent == false). + // + // We promote _value to T using `as T` instead of `!` operator to handle + // the case when _value is legitimately null (and T is a nullable type). + // `!` operator would always throw if _value is null. + return _value as T; + } + + /// True when this web setting instance contains a value. + /// + /// When false the [WebSetting.value] getter throws. + final bool isPresent; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + final WebSetting typedOther = other as WebSetting; + return typedOther.isPresent == isPresent && typedOther._value == _value; + } + + @override + int get hashCode => hashValues(_value, isPresent); +} + +/// Settings for configuring a WebViewPlatform. +/// +/// Initial settings are passed as part of [CreationParams], settings updates are sent with +/// [WebViewPlatform#updateSettings]. +/// +/// The `userAgent` parameter must not be null. +class WebSettings { + /// Construct an instance with initial settings. Future setting changes can be + /// sent with [WebviewPlatform#updateSettings]. + /// + /// The `userAgent` parameter must not be null. + WebSettings({ + this.javascriptMode, + this.hasNavigationDelegate, + this.hasProgressTracking, + this.debuggingEnabled, + this.gestureNavigationEnabled, + this.allowsInlineMediaPlayback, + required this.userAgent, + }) : assert(userAgent != null); + + /// The JavaScript execution mode to be used by the webview. + final JavascriptMode? javascriptMode; + + /// Whether the [WebView] has a [NavigationDelegate] set. + final bool? hasNavigationDelegate; + + /// Whether the [WebView] should track page loading progress. + /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. + final bool? hasProgressTracking; + + /// Whether to enable the platform's webview content debugging tools. + /// + /// See also: [WebView.debuggingEnabled]. + final bool? debuggingEnabled; + + /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. + /// + /// This will have no effect on Android. + final bool? allowsInlineMediaPlayback; + + /// The value used for the HTTP `User-Agent:` request header. + /// + /// If [userAgent.value] is null the platform's default user agent should be used. + /// + /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the + /// last time it was set. + /// + /// See also [WebView.userAgent]. + final WebSetting userAgent; + + /// Whether to allow swipe based navigation in iOS. + /// + /// See also: [WebView.gestureNavigationEnabled] + final bool? gestureNavigationEnabled; + + @override + String toString() { + return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart new file mode 100644 index 000000000000..b508989ed978 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/platform_interface/platform_interface.dart'; +export 'src/types/types.dart'; +export 'src/method_channel/webview_method_channel.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..bf43c265d77a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: webview_flutter_platform_interface +description: A common platform interface for the webview_flutter plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pedantic: ^1.10.0 \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart new file mode 100644 index 000000000000..2f845eaa4999 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -0,0 +1,457 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Tests on `plugin.flutter.io/webview_` channel', () { + const int channelId = 1; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/webview_$channelId'); + final WebViewPlatformCallbacksHandler callbacksHandler = + MockWebViewPlatformCallbacksHandler(); + final JavascriptChannelRegistry javascriptChannelRegistry = + MockJavascriptChannelRegistry(); + + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + switch (methodCall.method) { + case 'currentUrl': + return 'https://test.url'; + case 'canGoBack': + case 'canGoForward': + return true; + case 'evaluateJavascript': + return methodCall.arguments as String; + case 'getScrollX': + return 10; + case 'getScrollY': + return 20; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + final MethodChannelWebViewPlatform webViewPlatform = + MethodChannelWebViewPlatform( + channelId, + callbacksHandler, + javascriptChannelRegistry, + ); + + tearDown(() { + log.clear(); + }); + + test('loadUrl with headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + const { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + }, + ), + ], + ); + }); + + test('loadUrl without headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + null, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': null, + }, + ), + ], + ); + }); + + test('currentUrl', () async { + final String? currentUrl = await webViewPlatform.currentUrl(); + + expect(currentUrl, 'https://test.url'); + expect( + log, + [ + isMethodCall( + 'currentUrl', + arguments: null, + ), + ], + ); + }); + + test('canGoBack', () async { + final bool canGoBack = await webViewPlatform.canGoBack(); + + expect(canGoBack, true); + expect( + log, + [ + isMethodCall( + 'canGoBack', + arguments: null, + ), + ], + ); + }); + + test('canGoForward', () async { + final bool canGoForward = await webViewPlatform.canGoForward(); + + expect(canGoForward, true); + expect( + log, + [ + isMethodCall( + 'canGoForward', + arguments: null, + ), + ], + ); + }); + + test('goBack', () async { + await webViewPlatform.goBack(); + + expect( + log, + [ + isMethodCall( + 'goBack', + arguments: null, + ), + ], + ); + }); + + test('goForward', () async { + await webViewPlatform.goForward(); + + expect( + log, + [ + isMethodCall( + 'goForward', + arguments: null, + ), + ], + ); + }); + + test('reload', () async { + await webViewPlatform.reload(); + + expect( + log, + [ + isMethodCall( + 'reload', + arguments: null, + ), + ], + ); + }); + + test('clearCache', () async { + await webViewPlatform.clearCache(); + + expect( + log, + [ + isMethodCall( + 'clearCache', + arguments: null, + ), + ], + ); + }); + + test('updateSettings', () async { + final WebSettings settings = + WebSettings(userAgent: WebSetting.of('Dart Test')); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + }, + ), + ], + ); + }); + + test('updateSettings all parameters', () async { + final WebSettings settings = WebSettings( + userAgent: WebSetting.of('Dart Test'), + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: true, + hasProgressTracking: true, + debuggingEnabled: true, + gestureNavigationEnabled: true, + allowsInlineMediaPlayback: true, + ); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + 'jsMode': 0, + 'hasNavigationDelegate': true, + 'hasProgressTracking': true, + 'debuggingEnabled': true, + 'gestureNavigationEnabled': true, + 'allowsInlineMediaPlayback': true, + }, + ), + ], + ); + }); + + test('updateSettings without settings', () async { + final WebSettings settings = + WebSettings(userAgent: WebSetting.absent()); + await webViewPlatform.updateSettings(settings); + + expect( + log.isEmpty, + true, + ); + }); + + test('evaluateJavascript', () async { + final String evaluateJavascript = + await webViewPlatform.evaluateJavascript( + 'This simulates some Javascript code.', + ); + + expect('This simulates some Javascript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'evaluateJavascript', + arguments: 'This simulates some Javascript code.', + ), + ], + ); + }); + + test('addJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('addJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('removeJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('removeJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('getTitle', () async { + final String? title = await webViewPlatform.getTitle(); + + expect(title, null); + expect( + log, + [ + isMethodCall('getTitle', arguments: null), + ], + ); + }); + + test('scrollTo', () async { + await webViewPlatform.scrollTo(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollTo', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('scrollBy', () async { + await webViewPlatform.scrollBy(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollBy', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('getScrollX', () async { + final int x = await webViewPlatform.getScrollX(); + + expect(x, 10); + expect( + log, + [ + isMethodCall( + 'getScrollX', + arguments: null, + ), + ], + ); + }); + + test('getScrollY', () async { + final int y = await webViewPlatform.getScrollY(); + + expect(y, 20); + expect( + log, + [ + isMethodCall( + 'getScrollY', + arguments: null, + ), + ], + ); + }); + }); + + group('Tests on `plugins.flutter.io/cookie_manager` channel', () { + const MethodChannel cookieChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + final List log = []; + cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + if (methodCall.method == 'clearCookies') { + return true; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('clearCookies', () async { + final bool clearCookies = + await MethodChannelWebViewPlatform.clearCookies(); + + expect(clearCookies, true); + expect( + log, + [ + isMethodCall( + 'clearCookies', + arguments: null, + ), + ], + ); + }); + }); +} + +class MockWebViewPlatformCallbacksHandler extends Mock + implements WebViewPlatformCallbacksHandler {} + +class MockJavascriptChannelRegistry extends Mock + implements JavascriptChannelRegistry {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart new file mode 100644 index 000000000000..55d0e1e13bd1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; + +void main() { + final Map _log = {}; + final Set _channels = { + JavascriptChannel( + name: 'js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_2'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_3', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_3'] = message.message, + ), + }; + + tearDown(() { + _log.clear(); + }); + + test('ctor should initialize with channels.', () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + for (final JavascriptChannel channel in _channels) { + expect(registry.channels[channel.name], channel); + } + }); + + test('onJavascriptChannelMessage should forward message on correct channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + registry.onJavascriptChannelMessage( + 'js_channel_2', + 'test message on channel 2', + ); + + expect( + _log, + containsPair( + 'js_channel_2', + 'test message on channel 2', + )); + }); + + test( + 'onJavascriptChannelMessage should throw ArgumentError when message arrives on non-existing channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect( + () => registry.onJavascriptChannelMessage( + 'js_channel_4', + 'test message on channel 2', + ), + throwsA( + isA().having((ArgumentError error) => error.message, + 'message', 'No channel registered with name js_channel_4.'), + )); + }); + + test( + 'updateJavascriptChannelsFromSet should clear all channels when null is supplied.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + registry.updateJavascriptChannelsFromSet(null); + + expect(registry.channels, isEmpty); + }); + + test('updateJavascriptChannelsFromSet should update registry with new set.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + final Set newChannels = { + JavascriptChannel( + name: 'new_js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'new_js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_2'] = message.message, + ), + }; + + registry.updateJavascriptChannelsFromSet(newChannels); + + expect(registry.channels.length, 2); + for (final JavascriptChannel channel in newChannels) { + expect(registry.channels[channel.name], channel); + } + }); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart new file mode 100644 index 000000000000..f481edda1edd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; + +void main() { + final List _validChars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'.split(''); + final List _commonInvalidChars = + r'`~!@#$%^&*()-=+[]{}\|"' ':;/?<>,. '.split(''); + final List _digits = List.generate(10, (int index) => index++); + + test( + 'ctor should create JavascriptChannel when name starts with a valid character followed by a number.', + () { + for (final String char in _validChars) { + for (final int digit in _digits) { + final JavascriptChannel channel = + JavascriptChannel(name: '$char$digit', onMessageReceived: (_) {}); + + expect(channel.name, '$char$digit'); + } + } + }); + + test('ctor should assert when channel name starts with a number.', () { + for (final int i in _digits) { + expect( + () => JavascriptChannel(name: '$i', onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + }); + + test('ctor should assert when channel contains invalid char.', () { + for (final String validChar in _validChars) { + for (final String invalidChar in _commonInvalidChars) { + expect( + () => JavascriptChannel( + name: validChar + invalidChar, onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + } + }); +} From d5b65742487f64166a73f116b925ce800c45dcd7 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 9 Sep 2021 15:30:32 -0700 Subject: [PATCH 021/259] [video_player] add support for content-uri based videos (#4330) --- .../video_player/video_player/CHANGELOG.md | 4 ++++ .../video_player/lib/video_player.dart | 21 +++++++++++++++++++ .../video_player/video_player/pubspec.yaml | 4 ++-- .../video_player/test/video_player_test.dart | 9 ++++++++ .../video_player_web/CHANGELOG.md | 4 ++++ .../video_player_web_test.dart | 11 ++++++++++ .../lib/video_player_web.dart | 3 +++ .../video_player_web/pubspec.yaml | 4 ++-- 8 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 9cb642a4db56..a82455231ecd 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Add `contentUri` based VideoPlayerController. + ## 2.1.15 * Ensured seekTo isn't called before video player is initialized. Fixes [#89259](https://github.com/flutter/flutter/issues/89259). diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b4c4b2b2a311..685563ae12c3 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -222,6 +222,21 @@ class VideoPlayerController extends ValueNotifier { httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); + /// Constructs a [VideoPlayerController] playing a video from a contentUri. + /// + /// This will load the video from the input content-URI. + /// This is supported on Android only. + VideoPlayerController.contentUri(Uri contentUri, + {this.closedCaptionFile, this.videoPlayerOptions}) + : assert(defaultTargetPlatform == TargetPlatform.android, + 'VideoPlayerController.contentUri is only supported on Android.'), + dataSource = contentUri.toString(), + dataSourceType = DataSourceType.contentUri, + package = null, + formatHint = null, + httpHeaders = const {}, + super(VideoPlayerValue(duration: Duration.zero)); + /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. final String dataSource; @@ -298,6 +313,12 @@ class VideoPlayerController extends ValueNotifier { uri: dataSource, ); break; + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, + uri: dataSource, + ); + break; } if (videoPlayerOptions?.mixWithOthers != null) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 7f6f608687cc..86eee3c9bf42 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.15 +version: 2.2.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -24,7 +24,7 @@ dependencies: flutter: sdk: flutter meta: ^1.3.0 - video_player_platform_interface: ^4.1.0 + video_player_platform_interface: ^4.2.0 # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. The exact value doesn't matter since 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 ad536f840c1d..5fdc1fbf4574 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -284,6 +284,15 @@ void main() { }); }); + test('contentUri', () async { + final VideoPlayerController controller = + VideoPlayerController.contentUri(Uri.parse('content://video')); + await controller.initialize(); + + expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + 'content://video'); + }); + test('dispose', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index a7a198db21e1..4eb7c9d610b5 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 + +* Adopt `video_player_platform_interface` 4.2 and opt out of `contentUri` data source. + ## 2.0.3 * Add `implements` to pubspec. 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 d3ad80c890f1..2a830c9c573d 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 @@ -68,6 +68,17 @@ void main() { throwsUnimplementedError); }); + testWidgets('cannot create from content URI', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.contentUri, + uri: 'content://video', + ), + ), + throwsUnimplementedError); + }); + testWidgets('can dispose', (WidgetTester tester) async { expect(VideoPlayerPlatform.instance.dispose(await textureId), completes); }); 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 f9e27d16725a..612d22d2eb3f 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -88,6 +88,9 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { case DataSourceType.file: return Future.error(UnimplementedError( 'web implementation of video_player cannot play local files')); + case DataSourceType.contentUri: + return Future.error(UnimplementedError( + 'web implementation of video_player cannot play content uri')); } final _VideoPlayer player = _VideoPlayer( diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index c5eb57c1fc6e..b401673c628d 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/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter meta: ^1.3.0 - video_player_platform_interface: ^4.0.0 + video_player_platform_interface: ^4.2.0 dev_dependencies: flutter_test: From 5e4b5e0330ab3b7525331ef853a4c06fc8e068e9 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 10 Sep 2021 09:52:03 +0200 Subject: [PATCH 022/259] [webview_flutter] Add download listener to Android webview (#4322) --- .../webview_flutter/CHANGELOG.md | 3 +- .../FlutterDownloadListener.java | 33 ++++++++++ .../webviewflutter/FlutterWebView.java | 29 ++++++--- .../webviewflutter/FlutterWebViewClient.java | 16 +++++ .../webviewflutter/WebViewBuilder.java | 16 ++++- .../FlutterDownloadListenerTest.java | 42 +++++++++++++ .../FlutterWebViewClientTest.java | 60 +++++++++++++++++++ .../webviewflutter/FlutterWebViewTest.java | 7 ++- .../webviewflutter/WebViewBuilderTest.java | 7 ++- .../webview_flutter/pubspec.yaml | 2 +- 10 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java create mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java create mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 361bfd24f3af..1e1d5aa523ba 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.13 +* Send URL of File to download to the NavigationDelegate on Android just like it is already done on iOS. * Updated Android lint settings. ## 2.0.12 diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java new file mode 100644 index 000000000000..cfad4e315514 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import android.webkit.WebView; + +/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ +public class FlutterDownloadListener implements DownloadListener { + private final FlutterWebViewClient webViewClient; + private WebView webView; + + public FlutterDownloadListener(FlutterWebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ + public void setWebView(WebView webView) { + this.webView = webView; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + webViewClient.notifyDownload(webView, url); + } +} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index a3b681f27980..4651a5f5ae22 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -11,12 +11,14 @@ import android.os.Handler; import android.os.Message; import android.view.View; +import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -94,18 +96,25 @@ public void onProgressChanged(WebView view, int progress) { (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); + this.methodChannel = methodChannel; + this.methodChannel.setMethodCallHandler(this); + + flutterWebViewClient = new FlutterWebViewClient(methodChannel); + + FlutterDownloadListener flutterDownloadListener = + new FlutterDownloadListener(flutterWebViewClient); webView = createWebView( - new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient()); + new WebViewBuilder(context, containerView), + params, + new FlutterWebChromeClient(), + flutterDownloadListener); + flutterDownloadListener.setWebView(webView); displayListenerProxy.onPostWebViewInitialization(displayManager); platformThreadHandler = new Handler(context.getMainLooper()); - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); Map settings = (Map) params.get("settings"); if (settings != null) { applySettings(settings); @@ -156,7 +165,10 @@ public void onProgressChanged(WebView view, int progress) { */ @VisibleForTesting static WebView createWebView( - WebViewBuilder webViewBuilder, Map params, WebChromeClient webChromeClient) { + WebViewBuilder webViewBuilder, + Map params, + WebChromeClient webChromeClient, + @Nullable DownloadListener downloadListener) { boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); webViewBuilder .setUsesHybridComposition(usesHybridComposition) @@ -164,8 +176,9 @@ static WebView createWebView( .setJavaScriptCanOpenWindowsAutomatically( true) // Always allow automatically opening of windows. .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient( - webChromeClient); // Always use {@link FlutterWebChromeClient} as web Chrome client. + .setWebChromeClient(webChromeClient) + .setDownloadListener( + downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. return webViewBuilder.build(); } diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index adc84671a701..260ef8e8b15d 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -115,6 +115,22 @@ boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } + /** + * Notifies the Flutter code that a download should start when a navigation delegate is set. + * + * @param view the webView the result of the navigation delegate will be send to. + * @param url the download url + * @return A boolean whether or not the request is forwarded to the Flutter code. + */ + boolean notifyDownload(WebView view, String url) { + if (!hasNavigationDelegate) { + return false; + } + + notifyOnNavigationRequest(url, null, view, true); + return true; + } + private void onPageStarted(WebView view, String url) { Map args = new HashMap<>(); args.put("url", url); diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java index 6b8cc51febe8..d3cd1d57cdae 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -6,6 +6,7 @@ import android.content.Context; import android.view.View; +import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; @@ -44,6 +45,7 @@ static WebView create(Context context, boolean usesHybridComposition, View conta private boolean supportMultipleWindows; private boolean usesHybridComposition; private WebChromeClient webChromeClient; + private DownloadListener downloadListener; /** * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link @@ -122,6 +124,18 @@ public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClie return this; } + /** + * Registers the interface to be used when content can not be handled by the rendering engine, and + * should be downloaded instead. This will replace the current handler. + * + * @param downloadListener an implementation of DownloadListener This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { + this.downloadListener = downloadListener; + return this; + } + /** * Build the {@link android.webkit.WebView} using the current settings. * @@ -135,7 +149,7 @@ public WebView build() { webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); webSettings.setSupportMultipleWindows(supportMultipleWindows); webView.setWebChromeClient(webChromeClient); - + webView.setDownloadListener(downloadListener); return webView; } } diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java new file mode 100644 index 000000000000..2c918584ba83 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.webkit.WebView; +import org.junit.Before; +import org.junit.Test; + +public class FlutterDownloadListenerTest { + private FlutterWebViewClient webViewClient; + private WebView webView; + + @Before + public void before() { + webViewClient = mock(FlutterWebViewClient.class); + webView = mock(WebView.class); + } + + @Test + public void onDownloadStart_should_notify_webViewClient() { + String url = "testurl.com"; + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); + } + + @Test + public void onDownloadStart_should_pass_webView() { + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.setWebView(webView); + downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(eq(webView), anyString()); + } +} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java new file mode 100644 index 000000000000..86346ac08f16 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import android.webkit.WebView; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class FlutterWebViewClientTest { + + MethodChannel mockMethodChannel; + WebView mockWebView; + + @Before + public void before() { + mockMethodChannel = mock(MethodChannel.class); + mockWebView = mock(WebView.class); + } + + @Test + public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(true); + + client.notifyDownload(mockWebView, url); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); + verify(mockMethodChannel) + .invokeMethod( + eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); + HashMap map = (HashMap) argumentCaptor.getValue(); + assertEquals(map.get("url"), url); + assertEquals(map.get("isForMainFrame"), true); + } + + @Test + public void + notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(false); + + client.notifyDownload(mockWebView, url); + verifyNoInteractions(mockMethodChannel); + } +} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java index 96cbdece387c..56d9db1ee493 100644 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebView; import java.util.HashMap; @@ -20,6 +21,7 @@ public class FlutterWebViewTest { private WebChromeClient mockWebChromeClient; + private DownloadListener mockDownloadListener; private WebViewBuilder mockWebViewBuilder; private WebView mockWebView; @@ -28,6 +30,7 @@ public void before() { mockWebChromeClient = mock(WebChromeClient.class); mockWebViewBuilder = mock(WebViewBuilder.class); mockWebView = mock(WebView.class); + mockDownloadListener = mock(DownloadListener.class); when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) @@ -36,6 +39,8 @@ public void before() { when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) + .thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.build()).thenReturn(mockWebView); } @@ -43,7 +48,7 @@ public void before() { @Test public void createWebView_should_create_webview_with_default_configuration() { FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient); + mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java index 48fbce231ed5..423cb210c392 100644 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java @@ -9,6 +9,7 @@ import android.content.Context; import android.view.View; +import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; @@ -60,6 +61,7 @@ public void ctor_test() { public void build_should_set_values() throws IOException { WebSettings mockWebSettings = mock(WebSettings.class); WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + DownloadListener mockDownloadListener = mock(DownloadListener.class); when(mockWebView.getSettings()).thenReturn(mockWebSettings); @@ -68,7 +70,8 @@ public void build_should_set_values() throws IOException { .setDomStorageEnabled(true) .setJavaScriptCanOpenWindowsAutomatically(true) .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient); + .setWebChromeClient(mockWebChromeClient) + .setDownloadListener(mockDownloadListener); WebView webView = builder.build(); @@ -77,6 +80,7 @@ public void build_should_set_values() throws IOException { verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); verify(mockWebSettings).setSupportMultipleWindows(true); verify(mockWebView).setWebChromeClient(mockWebChromeClient); + verify(mockWebView).setDownloadListener(mockDownloadListener); } @Test @@ -95,5 +99,6 @@ public void build_should_use_default_values() throws IOException { verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); verify(mockWebSettings).setSupportMultipleWindows(false); verify(mockWebView).setWebChromeClient(null); + verify(mockWebView).setDownloadListener(null); } } diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index cc5d9cdc8b96..3976ff74fef6 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.12 +version: 2.0.13 environment: sdk: ">=2.12.0 <3.0.0" From b38c4e491afda039afc766e366bd0aeb813ab3c7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 10 Sep 2021 16:07:06 -0400 Subject: [PATCH 023/259] [flutter_plugin_tools] Remove an unnecessary logging message (#4320) --- script/tool/lib/src/publish_plugin_command.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 769b9e8c8f00..4fdecf603eec 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -433,7 +433,6 @@ final String _credentialsPath = () { // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 String? cacheDir; final String? pubCache = io.Platform.environment['PUB_CACHE']; - print(pubCache); if (pubCache != null) { cacheDir = pubCache; } else if (io.Platform.isWindows) { From a4f0e88fca1e8ea1e76913695ffb365646d9a039 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 10 Sep 2021 16:07:16 -0400 Subject: [PATCH 024/259] [flutter_plugin_tools] Make having no Java unit tests a failure (#4310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This brings the native Android unit tests in line with the policy of having tests that we expect all plugins to have—unless there's a very specific reason to opt them out—fail when missing instead of skipping when missing, to help guard against errors where we silently fail to run tests we think we are running. Adds an explicit exclusion list covering the plugins that have a reason to be opted out. Android portion of flutter/flutter#85469 --- .cirrus.yml | 3 +- .../configs/exclude_native_unit_android.yaml | 11 +++ script/tool/CHANGELOG.md | 5 + script/tool/lib/src/native_test_command.dart | 15 ++- .../tool/test/native_test_command_test.dart | 97 +++++++++++++++++-- 5 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 script/configs/exclude_native_unit_android.yaml diff --git a/.cirrus.yml b/.cirrus.yml index 9ca3a875c01e..a118942580e6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -182,7 +182,8 @@ task: - export CIRRUS_COMMIT_MESSAGE="" # Native integration tests are handled by firebase-test-lab below, so # only run unit tests. - - ./script/tool_runner.sh native-test --android --no-integration # must come after apk build + # Must come after build-examples. + - ./script/tool_runner.sh native-test --android --no-integration --exclude script/configs/exclude_native_unit_android.yaml firebase_test_lab_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml new file mode 100644 index 000000000000..5ec80eee73a0 --- /dev/null +++ b/script/configs/exclude_native_unit_android.yaml @@ -0,0 +1,11 @@ +# Deprecated; no plan to backfill the missing files +- android_alarm_manager +- battery +- device_info/device_info +- package_info +- sensors +- share +- wifi_info_flutter/wifi_info_flutter + +# No need for unit tests: +- espresso diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index aa73c65f3e80..c585bee47206 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- `native-test --android` now fails plugins that don't have unit tests, + rather than skipping them. + ## 0.7.1 - Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index e50878db7906..78a82afc571c 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -242,7 +242,8 @@ this command. final Iterable examples = plugin.getExamples(); - bool ranTests = false; + bool ranUnitTests = false; + bool ranAnyTests = false; bool failed = false; bool hasMissingBuild = false; for (final RepositoryPackage example in examples) { @@ -289,7 +290,8 @@ this command. printError('$exampleName unit tests failed.'); failed = true; } - ranTests = true; + ranUnitTests = true; + ranAnyTests = true; } if (runIntegrationTests) { @@ -311,7 +313,7 @@ this command. printError('$exampleName integration tests failed.'); failed = true; } - ranTests = true; + ranAnyTests = true; } } @@ -321,7 +323,12 @@ this command. ? 'Examples must be built before testing.' : null); } - if (!ranTests) { + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + if (!ranAnyTests) { return _PlatformResult(RunState.skipped); } return _PlatformResult(RunState.succeeded); diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index d1ab11f6e50d..f7b2ea5c0de8 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -430,7 +430,8 @@ void main() { ], ); - await runCapturingPrint(runner, ['native-test', '--android']); + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); final Directory androidFolder = plugin.childDirectory('example').childDirectory('android'); @@ -467,7 +468,8 @@ void main() { ], ); - await runCapturingPrint(runner, ['native-test', '--android']); + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); // Nothing should run since those files are all // integration_test-specific. @@ -641,7 +643,11 @@ void main() { ); final List output = await runCapturingPrint( - runner, ['native-test', '--android']); + runner, ['native-test', '--android'], + errorHandler: (Error e) { + // Having no unit tests is fatal, but that's not the point of this + // test so just ignore the failure. + }); expect( output, @@ -654,7 +660,7 @@ void main() { ])); }); - test('fails when a test fails', () async { + test('fails when a unit test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, @@ -695,6 +701,84 @@ void main() { ); }); + test('fails when an integration test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(), // unit passes + MockProcess(exitCode: 1), // integration fails + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example integration tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin/example'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).') + ]), + ); + }); + test('skips if Android is not supported', () async { createFakePlugin( 'plugin', @@ -713,7 +797,7 @@ void main() { ); }); - test('skips when running no tests', () async { + test('skips when running no tests in integration-only mode', () async { createFakePlugin( 'plugin', packagesDir, @@ -723,12 +807,11 @@ void main() { ); final List output = await runCapturingPrint( - runner, ['native-test', '--android']); + runner, ['native-test', '--android', '--no-unit']); expect( output, containsAllInOrder([ - contains('No Android unit tests found for plugin/example'), contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), From 70c314ce53cfd7c85d0ab498c8aaf334a69262d4 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 10 Sep 2021 17:36:22 -0700 Subject: [PATCH 025/259] [google_maps_flutter_web] Fix getScreenCoordinate, zIndex of Circles (#4298) This commit: * uses the zIndex attribute when converting Circle geometry objects. * ensures that the getScreenCoordinate method works as expected on the web platform. * adds tests that can use a fully-rendered Google Map (see projection_test.dart) * changes the initialization flow of the web Google Map, so the Controller is only returned to the main plugin when it's ready to work. In order to test the getScreenCoordinate method, the Controller of a fully-rendered map must be available on the test, so we can retrieve information from an actual map instance. While working on this, it was observed that the Controller was being sent to the programmer before it was truly ready (while the map was still initializing). Instead of littering the test with imprecise timeouts that may make these tests slower (and flakier) than needed, this PR also changes the initialization process of a GMap slightly so when its Controller is returned to the user of the plugin (onPlatformViewCreated method call), it is truly ready. For this: * Controller.init is immediately called after the controller is created, * The plugin waits for the first onTilesloaded event coming from the JS SDK, and then * The Controller is sent to the user This change happens within "private" sections of the plugin, so programmers using the plugin "normally" shouldn't notice any difference whatsoever (only that the GMap might load slightly faster, and the onPlatformViewCreated callback might be firing a few hundred milliseconds later). --- .../google_maps_flutter_web/CHANGELOG.md | 7 + .../google_maps_flutter_web/LICENSE | 26 ++ .../google_maps_controller_test.dart | 19 +- .../google_maps_controller_test.mocks.dart | 77 ++--- .../google_maps_plugin_test.dart | 60 ++-- .../google_maps_plugin_test.mocks.dart | 120 +++++--- .../integration_test/projection_test.dart | 265 ++++++++++++++++++ .../example/pubspec.yaml | 6 +- .../lib/google_maps_flutter_web.dart | 1 + .../lib/src/convert.dart | 7 +- .../lib/src/google_maps_controller.dart | 46 ++- .../lib/src/google_maps_flutter_web.dart | 14 +- .../third_party/to_screen_location/LICENSE | 21 ++ .../third_party/to_screen_location/README.md | 14 + .../to_screen_location.dart | 57 ++++ .../google_maps_flutter_web/pubspec.yaml | 4 +- .../tool/lib/src/license_check_command.dart | 24 +- 17 files changed, 636 insertions(+), 132 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart 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 83ffe09b357d..4d7ecf74e098 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,10 @@ +## 0.3.1 + +* Fix the `getScreenCoordinate(LatLng)` method. [#80710](https://github.com/flutter/flutter/issues/80710) +* Wait until the map tiles have loaded before calling `onPlatformViewCreated`, so +the returned controller is 100% functional (has bounds, a projection, etc...) +* Use zIndex property when initializing Circle objects. [#89374](https://github.com/flutter/flutter/issues/89374) + ## 0.3.0+4 * Add `implements` to pubspec. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE index c6823b81eb84..8f8c01d50118 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE @@ -1,3 +1,5 @@ +google_maps_flutter_web + Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, @@ -23,3 +25,27 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +to_screen_location + +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 1d33eea4c7f3..39aa641b10e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -257,13 +257,19 @@ void main() { }); testWidgets('renders initial geometry', (WidgetTester tester) async { - controller = _createController(circles: { - Circle(circleId: CircleId('circle-1')) - }, markers: { + controller = _createController(circles: { + Circle( + circleId: CircleId('circle-1'), + zIndex: 1234, + ), + }, markers: { Marker( - markerId: MarkerId('marker-1'), - infoWindow: InfoWindow( - title: 'title for test', snippet: 'snippet for test')) + markerId: MarkerId('marker-1'), + infoWindow: InfoWindow( + title: 'title for test', + snippet: 'snippet for test', + ), + ), }, polygons: { Polygon(polygonId: PolygonId('polygon-1'), points: [ LatLng(43.355114, -5.851333), @@ -315,6 +321,7 @@ void main() { .captured[0] as Set; expect(capturedCircles.first.circleId.value, 'circle-1'); + expect(capturedCircles.first.zIndex, 1234); expect(capturedMarkers.first.markerId.value, 'marker-1'); expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); expect(capturedMarkers.first.infoWindow.title, 'title for test'); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index 47933285b208..af8ed5420a0c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -2,26 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.15 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. -import 'package:google_maps/src/generated/google_maps_core.js.g.dart' as _i2; -import 'package:google_maps_flutter_platform_interface/src/types/circle.dart' +import 'package:google_maps/google_maps.dart' as _i2; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i4; -import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' - as _i7; -import 'package:google_maps_flutter_platform_interface/src/types/polygon.dart' - as _i5; -import 'package:google_maps_flutter_platform_interface/src/types/polyline.dart' - as _i6; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: 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 -class _FakeGMap extends _i1.Fake implements _i2.GMap {} +class _FakeGMap_0 extends _i1.Fake implements _i2.GMap {} /// A class which mocks [CirclesController]. /// @@ -34,7 +33,7 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { as Map<_i4.CircleId, _i3.CircleController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -62,6 +61,8 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [PolygonsController]. @@ -70,13 +71,13 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { class MockPolygonsController extends _i1.Mock implements _i3.PolygonsController { @override - Map<_i5.PolygonId, _i3.PolygonController> get polygons => + Map<_i4.PolygonId, _i3.PolygonController> get polygons => (super.noSuchMethod(Invocation.getter(#polygons), - returnValue: <_i5.PolygonId, _i3.PolygonController>{}) - as Map<_i5.PolygonId, _i3.PolygonController>); + returnValue: <_i4.PolygonId, _i3.PolygonController>{}) + as Map<_i4.PolygonId, _i3.PolygonController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -89,21 +90,23 @@ class MockPolygonsController extends _i1.Mock super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addPolygons(Set<_i5.Polygon>? polygonsToAdd) => + void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]), returnValueForMissingStub: null); @override - void changePolygons(Set<_i5.Polygon>? polygonsToChange) => + void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]), returnValueForMissingStub: null); @override - void removePolygons(Set<_i5.PolygonId>? polygonIdsToRemove) => super + void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => super .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]), returnValueForMissingStub: null); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [PolylinesController]. @@ -112,13 +115,13 @@ class MockPolygonsController extends _i1.Mock class MockPolylinesController extends _i1.Mock implements _i3.PolylinesController { @override - Map<_i6.PolylineId, _i3.PolylineController> get lines => + Map<_i4.PolylineId, _i3.PolylineController> get lines => (super.noSuchMethod(Invocation.getter(#lines), - returnValue: <_i6.PolylineId, _i3.PolylineController>{}) - as Map<_i6.PolylineId, _i3.PolylineController>); + returnValue: <_i4.PolylineId, _i3.PolylineController>{}) + as Map<_i4.PolylineId, _i3.PolylineController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -131,21 +134,23 @@ class MockPolylinesController extends _i1.Mock super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addPolylines(Set<_i6.Polyline>? polylinesToAdd) => + void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]), returnValueForMissingStub: null); @override - void changePolylines(Set<_i6.Polyline>? polylinesToChange) => super + void changePolylines(Set<_i4.Polyline>? polylinesToChange) => super .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]), returnValueForMissingStub: null); @override - void removePolylines(Set<_i6.PolylineId>? polylineIdsToRemove) => super + void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => super .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]), returnValueForMissingStub: null); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } /// A class which mocks [MarkersController]. @@ -153,13 +158,13 @@ class MockPolylinesController extends _i1.Mock /// See the documentation for Mockito's code generation for more information. class MockMarkersController extends _i1.Mock implements _i3.MarkersController { @override - Map<_i7.MarkerId, _i3.MarkerController> get markers => + Map<_i4.MarkerId, _i3.MarkerController> get markers => (super.noSuchMethod(Invocation.getter(#markers), - returnValue: <_i7.MarkerId, _i3.MarkerController>{}) - as Map<_i7.MarkerId, _i3.MarkerController>); + returnValue: <_i4.MarkerId, _i3.MarkerController>{}) + as Map<_i4.MarkerId, _i3.MarkerController>); @override _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap()) as _i2.GMap); + returnValue: _FakeGMap_0()) as _i2.GMap); @override set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), @@ -172,31 +177,33 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { super.noSuchMethod(Invocation.setter(#mapId, _mapId), returnValueForMissingStub: null); @override - void addMarkers(Set<_i7.Marker>? markersToAdd) => + void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]), returnValueForMissingStub: null); @override - void changeMarkers(Set<_i7.Marker>? markersToChange) => + void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]), returnValueForMissingStub: null); @override - void removeMarkers(Set<_i7.MarkerId>? markerIdsToRemove) => + void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]), returnValueForMissingStub: null); @override - void showMarkerInfoWindow(_i7.MarkerId? markerId) => + void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]), returnValueForMissingStub: null); @override - void hideMarkerInfoWindow(_i7.MarkerId? markerId) => + void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]), returnValueForMissingStub: null); @override - bool isInfoWindowShown(_i7.MarkerId? markerId) => + bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), returnValue: false) as bool); @override void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); + @override + String toString() => super.toString(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart index 2de431a5445e..758294f5bb91 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -28,16 +28,18 @@ void main() { group('GoogleMapsPlugin', () { late MockGoogleMapController controller; late GoogleMapsPlugin plugin; - int? reportedMapId; + late Completer reportedMapIdCompleter; + int numberOnPlatformViewCreatedCalls = 0; void onPlatformViewCreated(int id) { - reportedMapId = id; + reportedMapIdCompleter.complete(id); + numberOnPlatformViewCreatedCalls++; } setUp(() { controller = MockGoogleMapController(); plugin = GoogleMapsPlugin(); - reportedMapId = null; + reportedMapIdCompleter = Completer(); }); group('init/dispose', () { @@ -52,12 +54,6 @@ void main() { plugin.debugSetMapById({0: controller}); }); - testWidgets('init initializes controller', (WidgetTester tester) async { - await plugin.init(0); - - verify(controller.init()); - }); - testWidgets('cannot call methods after dispose', (WidgetTester tester) async { plugin.dispose(mapId: 0); @@ -95,17 +91,17 @@ void main() { reason: 'view type should contain the mapId passed when creating the map.', ); - expect( - reportedMapId, - testMapId, - reason: 'Should call onPlatformViewCreated with the mapId', - ); expect(cache, contains(testMapId)); expect( cache[testMapId], isNotNull, reason: 'cached controller cannot be null.', ); + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); }); testWidgets('returns cached instance if it already exists', @@ -121,11 +117,41 @@ void main() { ); expect(widget, equals(expected)); + }); + + testWidgets( + 'asynchronously reports onPlatformViewCreated the first time it happens', + (WidgetTester tester) async { + final Map cache = {}; + plugin.debugSetMapById(cache); + + plugin.buildView( + testMapId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + ); + + // Simulate Google Maps JS SDK being "ready" + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); + + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); + expect( + await reportedMapIdCompleter.future, + testMapId, + reason: 'Should call onPlatformViewCreated with the mapId', + ); + + // Fire repeated event again... + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); expect( - reportedMapId, - isNull, + numberOnPlatformViewCreatedCalls, + equals(1), reason: - 'onPlatformViewCreated should not be called when returning a cached controller', + 'Should not call onPlatformViewCreated for the same controller multiple times', ); }); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 43150f63ef93..01908ce777e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -2,41 +2,34 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.15 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. -import 'dart:async' as _i5; +import 'dart:async' as _i2; -import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart' - as _i6; -import 'package:google_maps_flutter_platform_interface/src/types/camera.dart' - as _i7; -import 'package:google_maps_flutter_platform_interface/src/types/circle_updates.dart' - as _i8; -import 'package:google_maps_flutter_platform_interface/src/types/location.dart' - as _i2; -import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' - as _i12; -import 'package:google_maps_flutter_platform_interface/src/types/marker_updates.dart' - as _i11; -import 'package:google_maps_flutter_platform_interface/src/types/polygon_updates.dart' - as _i9; -import 'package:google_maps_flutter_platform_interface/src/types/polyline_updates.dart' - as _i10; -import 'package:google_maps_flutter_platform_interface/src/types/screen_coordinate.dart' +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i3; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis -class _FakeLatLngBounds extends _i1.Fake implements _i2.LatLngBounds {} +class _FakeStreamController_0 extends _i1.Fake + implements _i2.StreamController {} -class _FakeScreenCoordinate extends _i1.Fake implements _i3.ScreenCoordinate {} +class _FakeLatLngBounds_1 extends _i1.Fake implements _i3.LatLngBounds {} -class _FakeLatLng extends _i1.Fake implements _i2.LatLng {} +class _FakeScreenCoordinate_2 extends _i1.Fake implements _i3.ScreenCoordinate { +} + +class _FakeLatLng_3 extends _i1.Fake implements _i3.LatLng {} /// A class which mocks [GoogleMapController]. /// @@ -44,63 +37,98 @@ class _FakeLatLng extends _i1.Fake implements _i2.LatLng {} class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapController { @override - _i5.Stream<_i6.MapEvent> get events => + _i2.StreamController<_i3.MapEvent> get stream => + (super.noSuchMethod(Invocation.getter(#stream), + returnValue: _FakeStreamController_0<_i3.MapEvent>()) + as _i2.StreamController<_i3.MapEvent>); + @override + _i2.Stream<_i3.MapEvent> get events => (super.noSuchMethod(Invocation.getter(#events), - returnValue: Stream<_i6.MapEvent>.empty()) - as _i5.Stream<_i6.MapEvent>); + returnValue: Stream<_i3.MapEvent>.empty()) + as _i2.Stream<_i3.MapEvent>); + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + @override + void debugSetOverrides( + {_i4.DebugCreateMapFunction? createMap, + _i4.MarkersController? markers, + _i4.CirclesController? circles, + _i4.PolygonsController? polygons, + _i4.PolylinesController? polylines}) => + super.noSuchMethod( + Invocation.method(#debugSetOverrides, [], { + #createMap: createMap, + #markers: markers, + #circles: circles, + #polygons: polygons, + #polylines: polylines + }), + returnValueForMissingStub: null); + @override + void init() => super.noSuchMethod(Invocation.method(#init, []), + returnValueForMissingStub: null); @override void updateRawOptions(Map? optionsUpdate) => super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), returnValueForMissingStub: null); @override - _i5.Future<_i2.LatLngBounds> getVisibleRegion() => - (super.noSuchMethod(Invocation.method(#getVisibleRegion, []), - returnValue: Future.value(_FakeLatLngBounds())) - as _i5.Future<_i2.LatLngBounds>); + _i2.Future<_i3.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( + Invocation.method(#getVisibleRegion, []), + returnValue: Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1())) + as _i2.Future<_i3.LatLngBounds>); @override - _i5.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) => + _i2.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i3.LatLng? latLng) => (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]), - returnValue: Future.value(_FakeScreenCoordinate())) - as _i5.Future<_i3.ScreenCoordinate>); + returnValue: + Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2())) + as _i2.Future<_i3.ScreenCoordinate>); @override - _i5.Future<_i2.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => + _i2.Future<_i3.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]), - returnValue: Future.value(_FakeLatLng())) as _i5.Future<_i2.LatLng>); + returnValue: Future<_i3.LatLng>.value(_FakeLatLng_3())) + as _i2.Future<_i3.LatLng>); @override - _i5.Future moveCamera(_i7.CameraUpdate? cameraUpdate) => + _i2.Future moveCamera(_i3.CameraUpdate? cameraUpdate) => (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]), - returnValue: Future.value(null), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i2.Future); @override - _i5.Future getZoomLevel() => + _i2.Future getZoomLevel() => (super.noSuchMethod(Invocation.method(#getZoomLevel, []), - returnValue: Future.value(0.0)) as _i5.Future); + returnValue: Future.value(0.0)) as _i2.Future); @override - void updateCircles(_i8.CircleUpdates? updates) => + void updateCircles(_i3.CircleUpdates? updates) => super.noSuchMethod(Invocation.method(#updateCircles, [updates]), returnValueForMissingStub: null); @override - void updatePolygons(_i9.PolygonUpdates? updates) => + void updatePolygons(_i3.PolygonUpdates? updates) => super.noSuchMethod(Invocation.method(#updatePolygons, [updates]), returnValueForMissingStub: null); @override - void updatePolylines(_i10.PolylineUpdates? updates) => + void updatePolylines(_i3.PolylineUpdates? updates) => super.noSuchMethod(Invocation.method(#updatePolylines, [updates]), returnValueForMissingStub: null); @override - void updateMarkers(_i11.MarkerUpdates? updates) => + void updateMarkers(_i3.MarkerUpdates? updates) => super.noSuchMethod(Invocation.method(#updateMarkers, [updates]), returnValueForMissingStub: null); @override - void showInfoWindow(_i12.MarkerId? markerId) => + void showInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]), returnValueForMissingStub: null); @override - void hideInfoWindow(_i12.MarkerId? markerId) => + void hideInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]), returnValueForMissingStub: null); @override - bool isInfoWindowShown(_i12.MarkerId? markerId) => + bool isInfoWindowShown(_i3.MarkerId? markerId) => (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), returnValue: false) as bool); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart new file mode 100644 index 000000000000..8a5a62013538 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart @@ -0,0 +1,265 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// These tests render an app with a small map widget, and use its map controller +// to compute values of the default projection. + +// (Tests methods that can't be mocked in `google_maps_controller_test.dart`) + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' + show GoogleMap, GoogleMapController; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +// This value is used when comparing long~num, like LatLng values. +const _acceptableLatLngDelta = 0.0000000001; + +// This value is used when comparing pixel measurements, mostly to gloss over +// browser rounding errors. +const _acceptablePixelDelta = 1; + +/// Test Google Map Controller +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Methods that require a proper Projection', () { + final LatLng center = LatLng(43.3078, -5.6958); + final Size size = Size(320, 240); + final CameraPosition initialCamera = CameraPosition( + target: center, + zoom: 14, + ); + + late Completer controllerCompleter; + late void Function(GoogleMapController) onMapCreated; + + setUp(() { + controllerCompleter = Completer(); + onMapCreated = (GoogleMapController mapController) { + controllerCompleter.complete(mapController); + }; + }); + + group('getScreenCoordinate', () { + testWidgets('target of map is in center of widget', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(center); + + expect( + screenPosition.x, + closeTo(size.width / 2, _acceptablePixelDelta), + ); + expect( + screenPosition.y, + closeTo(size.height / 2, _acceptablePixelDelta), + ); + }); + + testWidgets('NorthWest of visible region corresponds to x:0, y:0', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(northWest); + + expect(screenPosition.x, closeTo(0, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(0, _acceptablePixelDelta)); + }); + + testWidgets( + 'SouthEast of visible region corresponds to x:size.width, y:size.height', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(southEast); + + expect(screenPosition.x, closeTo(size.width, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(size.height, _acceptablePixelDelta)); + }); + }); + + group('getLatLng', () { + testWidgets('Center of widget is the target of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width ~/ 2, y: size.height ~/ 2), + ); + + expect( + coords.latitude, + closeTo(center.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(center.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Top-left of widget is NorthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: 0, y: 0), + ); + + expect( + coords.latitude, + closeTo(northWest.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(northWest.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Bottom-right of widget is SouthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width.toInt(), y: size.height.toInt()), + ); + + expect( + coords.latitude, + closeTo(southEast.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(southEast.longitude, _acceptableLatLngDelta), + ); + }); + }); + }); +} + +// Pumps a CenteredMap Widget into a given tester, with some parameters +void pumpCenteredMap( + WidgetTester tester, { + required CameraPosition initialCamera, + Size size = const Size(320, 240), + void Function(GoogleMapController)? onMapCreated, +}) async { + await tester.pumpWidget( + CenteredMap( + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ), + ); + + // This is needed to kick-off the rendering of the JS Map flutter widget + await tester.pump(); +} + +/// Renders a Map widget centered on the screen. +/// This depends in `package:google_maps_flutter` to work. +class CenteredMap extends StatelessWidget { + const CenteredMap({ + required this.initialCamera, + required this.size, + required this.onMapCreated, + Key? key, + }) : super(key: key); + + /// A function that receives the [GoogleMapController] of the Map widget once initialized. + final void Function(GoogleMapController)? onMapCreated; + + /// The size of the rendered map widget. + final Size size; + + /// The initial camera position (center + zoom level) of the Map widget. + final CameraPosition initialCamera; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox.fromSize( + size: size, + child: GoogleMap( + initialCameraPosition: initialCamera, + onMapCreated: onMapCreated, + ), + ), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index b0ac9910afc9..249b893d198c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -13,8 +13,10 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^1.11.0 - google_maps: ^5.1.0 + build_runner: ^2.1.1 + google_maps: ^5.2.0 + google_maps_flutter: # Used for projection_test.dart + path: ../../google_maps_flutter http: ^0.13.0 mockito: ^5.0.0 flutter_driver: 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 6dc2dab572a6..0355f2923528 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 @@ -25,6 +25,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'src/third_party/to_screen_location/to_screen_location.dart'; import 'src/types.dart'; part 'src/google_maps_flutter_web.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 2e71c795ff0e..c026a03be804 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -264,7 +264,7 @@ gmaps.MarkerOptions _markerOptionsFromMarker( } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { - final populationOptions = gmaps.CircleOptions() + final circleOptions = gmaps.CircleOptions() ..strokeColor = _getCssColor(circle.strokeColor) ..strokeOpacity = _getCssOpacity(circle.strokeColor) ..strokeWeight = circle.strokeWidth @@ -272,8 +272,9 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { ..fillOpacity = _getCssOpacity(circle.fillColor) ..center = gmaps.LatLng(circle.center.latitude, circle.center.longitude) ..radius = circle.radius - ..visible = circle.visible; - return populationOptions; + ..visible = circle.visible + ..zIndex = circle.zIndex; + return circleOptions; } gmaps.PolygonOptions _polygonOptionsFromPolygon( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index 226268270579..edf47764f346 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -53,6 +53,10 @@ class GoogleMapController { // The StreamController used by this controller and the geometry ones. final StreamController _streamController; + /// The StreamController for the events of this Map. Only for integration testing. + @visibleForTesting + StreamController get stream => _streamController; + /// The Stream over which this controller broadcasts events. Stream get events => _streamController.stream; @@ -132,10 +136,27 @@ class GoogleMapController { return gmaps.GMap(div, options); } - /// Initializes the [gmaps.GMap] instance from the stored `rawOptions`. + /// A flag that returns true if the controller has been initialized or not. + @visibleForTesting + bool get isInitialized => _googleMap != null; + + /// Starts the JS Maps SDK into the target [_div] with `rawOptions`. + /// + /// (Also initializes the geometry/traffic layers.) + /// + /// The first part of this method starts the rendering of a [gmaps.GMap] inside + /// of the target [_div], with configuration from `rawOptions`. It then stores + /// the created GMap in the [_googleMap] attribute. /// - /// This method actually renders the GMap into the cached `_div`. This is - /// called by the [GoogleMapsPlugin.init] method when appropriate. + /// Not *everything* is rendered with the initial `rawOptions` configuration, + /// geometry and traffic layers (and possibly others in the future) have their + /// own configuration and are rendered on top of a GMap instance later. This + /// happens in the second half of this method. + /// + /// This method is eagerly called from the [GoogleMapsPlugin.buildView] method + /// so the internal [GoogleMapsController] of a Web Map initializes as soon as + /// possible. Check [_attachMapEvents] to see how this controller notifies the + /// plugin of it being fully ready (through the `onTilesloaded.first` event). /// /// Failure to call this method would result in the GMap not rendering at all, /// and most of the public methods on this class no-op'ing. @@ -151,6 +172,7 @@ class GoogleMapController { _attachMapEvents(map); _attachGeometryControllers(map); + // Now attach the geometry, traffic and any other layers... _renderInitialGeometry( markers: _markers, circles: _circles, @@ -163,6 +185,10 @@ class GoogleMapController { // Funnels map gmap events into the plugin's stream controller. void _attachMapEvents(gmaps.GMap map) { + map.onTilesloaded.first.then((event) { + // Report the map as ready to go the first time the tiles load + _streamController.add(WebMapReadyEvent(_mapId)); + }); map.onClick.listen((event) { assert(event.latLng != null); _streamController.add( @@ -292,14 +318,8 @@ class GoogleMapController { Future getScreenCoordinate(LatLng latLng) async { assert(_googleMap != null, 'Cannot get the screen coordinates with a null map.'); - assert(_googleMap!.projection != null, - 'Cannot compute screen coordinate with a null map or projection.'); - - final point = - _googleMap!.projection!.fromLatLngToPoint!(_latLngToGmLatLng(latLng))!; - assert(point.x != null && point.y != null, - 'The x and y of a ScreenCoordinate cannot be null.'); + final point = toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng)); return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt()); } @@ -403,3 +423,9 @@ class GoogleMapController { _streamController.close(); } } + +/// An event fired when a [mapId] on web is interactive. +class WebMapReadyEvent extends MapEvent { + /// Build a WebMapReady Event for the map represented by `mapId`. + WebMapReadyEvent(int mapId) : super(mapId, null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index 692917fef4da..d03dec93ce3f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -35,7 +35,10 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future init(int mapId) async { - _map(mapId).init(); + // The internal instance of our controller is initialized eagerly in `buildView`, + // so we don't have to do anything in this method, which is left intentionally + // blank. + assert(_map(mapId) != null, 'Must call buildWidget before init!'); } /// Updates the options of a given `mapId`. @@ -305,11 +308,16 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { polylines: polylines, circles: circles, mapOptions: mapOptions, - ); + )..init(); // Initialize the controller _mapById[creationId] = mapController; - onPlatformViewCreated.call(creationId); + mapController.events.whereType().first.then((event) { + assert(creationId == event.mapId, + 'Received WebMapReadyEvent for the wrong map'); + // Notify the plugin now that there's a fully initialized controller. + onPlatformViewCreated.call(event.mapId); + }); assert(mapController.widget != null, 'The widget of a GoogleMapController cannot be null before calling dispose on it.'); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE new file mode 100644 index 000000000000..ab4e163abe54 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md new file mode 100644 index 000000000000..8bd4a39c065f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md @@ -0,0 +1,14 @@ +# to_screen_location + +The code in this directory is a Dart re-implementation of Krasimir Tsonev's blog +post: [GoogleMaps API v3: convert LatLng object to actual pixels][blog-post]. + +The blog post describes a way to implement the [`toScreenLocation` method][method] +of the Google Maps Platform SDK for the web. + +Used under license (MIT), [available here][blog-license], and in the accompanying +LICENSE file. + +[blog-license]: https://krasimirtsonev.com/license +[blog-post]: https://krasimirtsonev.com/blog/article/google-maps-api-v3-convert-latlng-object-to-actual-pixels-point-object +[method]: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#toScreenLocation(com.google.android.libraries.maps.model.LatLng) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart new file mode 100644 index 000000000000..2963111fdcc3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart @@ -0,0 +1,57 @@ +// The MIT License (MIT) +// +// Copyright (c) 2008 Krasimir Tsonev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:google_maps/google_maps.dart' as gmaps; + +/// Returns a screen location that corresponds to a geographical coordinate ([gmaps.LatLng]). +/// +/// The screen location is in pixels relative to the top left of the Map widget +/// (not of the whole screen/app). +/// +/// See: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#public-point-toscreenlocation-latlng-location +gmaps.Point toScreenLocation(gmaps.GMap map, gmaps.LatLng coords) { + final zoom = map.zoom; + final bounds = map.bounds; + final projection = map.projection; + + assert( + bounds != null, 'Map Bounds required to compute screen x/y of LatLng.'); + assert(projection != null, + 'Map Projection required to compute screen x/y of LatLng.'); + assert(zoom != null, + 'Current map zoom level required to compute screen x/y of LatLng.'); + + final ne = bounds!.northEast; + final sw = bounds.southWest; + + final topRight = projection!.fromLatLngToPoint!(ne)!; + final bottomLeft = projection.fromLatLngToPoint!(sw)!; + + final scale = 1 << (zoom!.toInt()); // 2 ^ zoom + + final worldPoint = projection.fromLatLngToPoint!(coords)!; + + return gmaps.Point( + ((worldPoint.x! - bottomLeft.x!) * scale).toInt(), + ((worldPoint.y! - topRight.y!) * scale).toInt(), + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 82605f8fd070..8a23916b0e98 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.0+4 +version: 0.3.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps_flutter_platform_interface: ^2.0.1 - google_maps: ^5.1.0 + google_maps: ^5.2.0 meta: ^1.3.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index e68585c44bdf..8cee46b45a4c 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -49,16 +49,24 @@ const Set _ignoredFullBasenameList = { // When adding license regexes here, include the copyright info to ensure that // any new additions are flagged for added scrutiny in review. final List _thirdPartyLicenseBlockRegexes = [ -// Third-party code used in url_launcher_web. + // Third-party code used in url_launcher_web. RegExp( - r'^// Copyright 2017 Workiva Inc\..*' - r'^// Licensed under the Apache License, Version 2\.0', - multiLine: true, - dotAll: true), + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', + multiLine: true, + dotAll: true, + ), + // Third-party code used in google_maps_flutter_web. + RegExp( + r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', + multiLine: true, + ), // bsdiff in flutter/packages. - RegExp(r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' - r'// Use of this source code is governed by a BSD-style license that can be\n' - r'// found in the LICENSE file\.\n'), + RegExp( + r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n', + ), ]; // The exact format of the BSD license that our license files should contain. From cfc8a20a1d64ec33ad549344819dae63af76f88b Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Mon, 13 Sep 2021 11:47:06 -0700 Subject: [PATCH 026/259] renew cirrus key (#4340) --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index a118942580e6..d92c12cef61c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,4 @@ -gcp_credentials: ENCRYPTED[!0e63b52bd7e4fda1cd7b7bf2b4fe515a27fadbeaced01f5ad8b699b81d3611ed64c5d3271bcd8426dd914ef41cba48a0!] +gcp_credentials: ENCRYPTED[!48cff44dd32e9cc412d4d381c7fe68d373ca04cf2639f8192d21cb1a9ab5e21129651423a1cf88f3fd7fe2125c1cabd9!] # Don't run on release tags since it creates O(n^2) tasks where n is the # number of plugins From 4a98e239b131971e0a83d4383c709bc3205a333f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 13 Sep 2021 19:37:12 -0400 Subject: [PATCH 027/259] [flutter_plugin_tools] Make no unit tests fatal for iOS/macOS (#4341) Brings iOS and macOS into alignment with the other platforms, where having unit tests set up is required. - For deprecated plugins with no tests, `--exclude`s them, as on other platforms. - For `quick_actions` and `share`, which have integration tests but no unit tests, sets up the unit test scaffolding. (This is done for `share` even though it's deprecated since unlike other platforms, iOS/macOS runs both native tests in the same command, and setting up a special way to exclude just units tests for that one case would be much more effort.) Fixes flutter/flutter#85469 --- .cirrus.yml | 2 +- .../quick_actions/example/ios/Podfile | 3 + .../ios/Runner.xcodeproj/project.pbxproj | 105 +++++ .../xcshareddata/xcschemes/Runner.xcscheme | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/RunnerTests/Info.plist | 22 + .../example/ios/RunnerTests/RunnerTests.m | 18 + packages/share/example/ios/Podfile | 3 + .../ios/Runner.xcodeproj/project.pbxproj | 105 +++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../share/example/ios/RunnerTests/Info.plist | 22 + .../example/ios/RunnerTests/RunnerTests.m | 18 + script/configs/exclude_native_ios.yaml | 7 + script/tool/CHANGELOG.md | 4 +- script/tool/lib/src/native_test_command.dart | 50 ++- .../tool/test/native_test_command_test.dart | 397 ++++++++---------- 16 files changed, 543 insertions(+), 239 deletions(-) create mode 100644 packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/quick_actions/quick_actions/example/ios/RunnerTests/Info.plist create mode 100644 packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m create mode 100644 packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/share/example/ios/RunnerTests/Info.plist create mode 100644 packages/share/example/ios/RunnerTests/RunnerTests.m create mode 100644 script/configs/exclude_native_ios.yaml diff --git a/.cirrus.yml b/.cirrus.yml index d92c12cef61c..5b696031b304 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -256,7 +256,7 @@ task: xcode_analyze_script: - ./script/tool_runner.sh xcode-analyze --ios native_test_script: - - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" + - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" --exclude=script/configs/exclude_native_ios.yaml drive_script: # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. diff --git a/packages/quick_actions/quick_actions/example/ios/Podfile b/packages/quick_actions/quick_actions/example/ios/Podfile index f7d6a5e68c3a..3924e59aa0f9 100644 --- a/packages/quick_actions/quick_actions/example/ios/Podfile +++ b/packages/quick_actions/quick_actions/example/ios/Podfile @@ -29,6 +29,9 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj index ee150598f59b..745315bcc3d2 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686BE82F25E58CCF00862533 /* RunnerUITests.m */; }; 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; @@ -19,6 +20,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; 686BE83225E58CCF00862533 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -44,6 +52,9 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; + 33E20B3626EFCDFC00A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -65,6 +76,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 33E20B2F26EFCDFC00A4A191 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 686BE82A25E58CCF00862533 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -83,6 +101,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 33E20B3326EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */, + 33E20B3626EFCDFC00A4A191 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; 686BE82E25E58CCF00862533 /* RunnerUITests */ = { isa = PBXGroup; children = ( @@ -109,6 +136,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 686BE82E25E58CCF00862533 /* RunnerUITests */, + 33E20B3326EFCDFC00A4A191 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, D0FE95BE2380323DD75CB891 /* Pods */, A44AD0D63DEF785A2A2DEE28 /* Frameworks */, @@ -120,6 +148,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */, + 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -168,6 +197,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 33E20B3126EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 33E20B2E26EFCDFC00A4A191 /* Sources */, + 33E20B2F26EFCDFC00A4A191 /* Frameworks */, + 33E20B3026EFCDFC00A4A191 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 686BE82C25E58CCF00862533 /* RunnerUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; @@ -216,6 +263,10 @@ LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { + 33E20B3126EFCDFC00A4A191 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 686BE82C25E58CCF00862533 = { CreatedOnToolsVersion = 12.4; ProvisioningStyle = Automatic; @@ -241,11 +292,19 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, 686BE82C25E58CCF00862533 /* RunnerUITests */, + 33E20B3126EFCDFC00A4A191 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 33E20B3026EFCDFC00A4A191 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 686BE82B25E58CCF00862533 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -316,6 +375,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 33E20B2E26EFCDFC00A4A191 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 686BE82925E58CCF00862533 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -337,6 +404,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */; + }; 686BE83325E58CCF00862533 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; @@ -364,6 +436,30 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 33E20B3926EFCDFC00A4A191 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 33E20B3A26EFCDFC00A4A191 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; 686BE83425E58CCF00862533 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -560,6 +656,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33E20B3926EFCDFC00A4A191 /* Debug */, + 33E20B3A26EFCDFC00A4A191 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9850cc113026..ac798eda8c17 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -47,6 +47,16 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/quick_actions/quick_actions/example/ios/RunnerTests/Info.plist b/packages/quick_actions/quick_actions/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m new file mode 100644 index 000000000000..64e0f7e1d8b2 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import quick_actions; +@import XCTest; + +@interface QuickActionsTests : XCTestCase +@end + +@implementation QuickActionsTests + +- (void)testPlugin { + FLTQuickActionsPlugin* plugin = [[FLTQuickActionsPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/share/example/ios/Podfile b/packages/share/example/ios/Podfile index f7d6a5e68c3a..3924e59aa0f9 100644 --- a/packages/share/example/ios/Podfile +++ b/packages/share/example/ios/Podfile @@ -29,6 +29,9 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj index 16acd6a7b886..d7e896212533 100644 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 85392794417D70A970945C83 /* libPods-Runner.a */; }; 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */; }; + 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B4226EFCEF400A4A191 /* RunnerTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 683426AD2538D314009B194C /* FLTShareExampleUITests.m */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -19,6 +20,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; 683426B02538D314009B194C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -46,6 +54,9 @@ 1BCE6CBBA2E91FD0397A29C8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33E20B4226EFCEF400A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; + 33E20B4426EFCEF400A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 683426AB2538D314009B194C /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 683426AD2538D314009B194C /* FLTShareExampleUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTShareExampleUITests.m; sourceTree = ""; }; @@ -65,6 +76,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 33E20B3D26EFCEF400A4A191 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 683426A82538D314009B194C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -92,6 +110,15 @@ name = Pods; sourceTree = ""; }; + 33E20B4126EFCEF400A4A191 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33E20B4226EFCEF400A4A191 /* RunnerTests.m */, + 33E20B4426EFCEF400A4A191 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; 683426AC2538D314009B194C /* RunnerUITests */ = { isa = PBXGroup; children = ( @@ -126,6 +153,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 683426AC2538D314009B194C /* RunnerUITests */, + 33E20B4126EFCEF400A4A191 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 16DDF472245BCC3E62219493 /* Pods */, 8CA31EF57239BF20619316D9 /* Frameworks */, @@ -137,6 +165,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 683426AB2538D314009B194C /* RunnerUITests.xctest */, + 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -168,6 +197,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 33E20B3F26EFCEF400A4A191 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 33E20B3C26EFCEF400A4A191 /* Sources */, + 33E20B3D26EFCEF400A4A191 /* Frameworks */, + 33E20B3E26EFCEF400A4A191 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 683426AA2538D314009B194C /* RunnerUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */; @@ -216,6 +263,10 @@ LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { + 33E20B3F26EFCEF400A4A191 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 683426AA2538D314009B194C = { CreatedOnToolsVersion = 11.7; ProvisioningStyle = Automatic; @@ -241,11 +292,19 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, 683426AA2538D314009B194C /* RunnerUITests */, + 33E20B3F26EFCEF400A4A191 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 33E20B3E26EFCEF400A4A191 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 683426A92538D314009B194C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -316,6 +375,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 33E20B3C26EFCEF400A4A191 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 683426A72538D314009B194C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -337,6 +404,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */; + }; 683426B12538D314009B194C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; @@ -364,6 +436,30 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 33E20B4726EFCEF400A4A191 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 33E20B4826EFCEF400A4A191 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; 683426B22538D314009B194C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -560,6 +656,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33E20B4726EFCEF400A4A191 /* Debug */, + 33E20B4826EFCEF400A4A191 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/share/example/ios/RunnerTests/Info.plist b/packages/share/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/share/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/share/example/ios/RunnerTests/RunnerTests.m b/packages/share/example/ios/RunnerTests/RunnerTests.m new file mode 100644 index 000000000000..3c4c341fd451 --- /dev/null +++ b/packages/share/example/ios/RunnerTests/RunnerTests.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import share; +@import XCTest; + +@interface ShareTests : XCTestCase +@end + +@implementation ShareTests + +- (void)testPlugin { + FLTSharePlugin* plugin = [[FLTSharePlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/script/configs/exclude_native_ios.yaml b/script/configs/exclude_native_ios.yaml new file mode 100644 index 000000000000..723fcfa64715 --- /dev/null +++ b/script/configs/exclude_native_ios.yaml @@ -0,0 +1,7 @@ +# Deprecated; no plan to backfill the missing files +- battery +- connectivity/connectivity +- device_info/device_info +- package_info +- sensors +- wifi_info_flutter/wifi_info_flutter diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index c585bee47206..7df6913db7d1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,7 +1,7 @@ ## NEXT -- `native-test --android` now fails plugins that don't have unit tests, - rather than skipping them. +- `native-test --android`, `--ios`, and `--macos` now fail plugins that don't + have unit tests, rather than skipping them. ## 0.7.1 diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 78a82afc571c..4911b4aeb156 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -251,8 +251,6 @@ this command. final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); - // TODO(stuartmorgan): Make !hasUnitTests fatal. See - // https://github.com/flutter/flutter/issues/85469 if (mode.unit && !hasUnitTests) { _printNoExampleTestsMessage(example, 'Android unit'); } @@ -355,33 +353,40 @@ this command. List extraFlags = const [], }) async { String? testTarget; + const String unitTestTarget = 'RunnerTests'; if (mode.unitOnly) { - testTarget = 'RunnerTests'; + testTarget = unitTestTarget; } else if (mode.integrationOnly) { testTarget = 'RunnerUITests'; } + bool ranUnitTests = false; // Assume skipped until at least one test has run. RunState overallResult = RunState.skipped; for (final RepositoryPackage example in plugin.getExamples()) { final String exampleName = example.displayName; - // TODO(stuartmorgan): Always check for RunnerTests, and make it fatal if - // no examples have it. See - // https://github.com/flutter/flutter/issues/85469 - if (testTarget != null) { - final Directory project = example.directory - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); + // If running a specific target, check that. Otherwise, check if there + // are unit tests, since having no unit tests for a plugin is fatal + // (by repo policy) even if there are integration tests. + bool exampleHasUnitTests = false; + final String? targetToCheck = + testTarget ?? (mode.unit ? unitTestTarget : null); + final Directory xcodeProject = example.directory + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + if (targetToCheck != null) { final bool? hasTarget = - await _xcode.projectHasTarget(project, testTarget); + await _xcode.projectHasTarget(xcodeProject, targetToCheck); if (hasTarget == null) { printError('Unable to check targets for $exampleName.'); overallResult = RunState.failed; continue; } else if (!hasTarget) { - print('No "$testTarget" target in $exampleName; skipping.'); + print('No "$targetToCheck" target in $exampleName; skipping.'); continue; + } else if (targetToCheck == unitTestTarget) { + exampleHasUnitTests = true; } } @@ -404,20 +409,39 @@ this command. switch (exitCode) { case _xcodebuildNoTestExitCode: _printNoExampleTestsMessage(example, platform); - continue; + break; case 0: printSuccess('Successfully ran $platform xctest for $exampleName'); // If this is the first test, assume success until something fails. if (overallResult == RunState.skipped) { overallResult = RunState.succeeded; } + if (exampleHasUnitTests) { + ranUnitTests = true; + } break; default: // Any failure means a failure overall. overallResult = RunState.failed; + // If unit tests ran, note that even if they failed. + if (exampleHasUnitTests) { + ranUnitTests = true; + } break; } } + + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + // Only return a specific summary error message about the missing unit + // tests if there weren't also failures, to avoid having a misleadingly + // specific message. + if (overallResult != RunState.failed) { + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + } + return _PlatformResult(overallResult); } diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index f7b2ea5c0de8..ba93efcb3ace 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -78,6 +78,61 @@ void main() { runner.addCommand(command); }); + // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a + // project that contains [targets]. + MockProcess _getMockXcodebuildListProcess(List targets) { + final Map projects = { + 'project': { + 'targets': targets, + } + }; + return MockProcess(stdout: jsonEncode(projects)); + } + + // Returns the ProcessCall to expect for checking the targets present in + // the [package]'s [platform]/Runner.xcodeproj. + ProcessCall _getTargetCheckCall(Directory package, String platform) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + package + .childDirectory(platform) + .childDirectory('Runner.xcodeproj') + .path, + ], + null); + } + + // Returns the ProcessCall to expect for running the tests in the + // workspace [platform]/Runner.xcworkspace, with the given extra flags. + ProcessCall _getRunTestCall( + Directory package, + String platform, { + String? destination, + List extraFlags = const [], + }) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + 'test', + '-workspace', + '$platform/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + if (destination != null) ...['-destination', destination], + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + package.path); + } + test('fails if no platforms are provided', () async { Error? commandError; final List output = await runCapturingPrint( @@ -124,31 +179,26 @@ void main() { pluginDirectory1.childDirectory('example'); processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), // Exit code 66 from testing indicates no tests. MockProcess(exitCode: 66), ]; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); + final List output = await runCapturingPrint( + runner, ['native-test', '--macos', '--no-unit']); - expect(output, contains(contains('No tests found.'))); + expect( + output, + containsAllInOrder([ + contains('No tests found.'), + contains('Skipped 1 package(s)'), + ])); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -196,6 +246,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -213,22 +268,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), ])); }); @@ -243,6 +285,8 @@ void main() { processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; await runCapturingPrint(runner, ['native-test', '--ios']); @@ -261,22 +305,9 @@ void main() { '--json', ], null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), ])); }); }); @@ -325,6 +356,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--macos', @@ -338,20 +374,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -999,13 +1023,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - const Map projects = { - 'project': { - 'targets': ['RunnerTests', 'RunnerUITests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ @@ -1023,34 +1043,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerTests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerTests']), ])); }); @@ -1064,13 +1059,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - const Map projects = { - 'project': { - 'targets': ['RunnerTests', 'RunnerUITests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ @@ -1088,34 +1079,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-only-testing:RunnerUITests', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -1130,13 +1096,8 @@ void main() { pluginDirectory1.childDirectory('example'); // Simulate a project with unit tests but no integration tests... - const Map projects = { - 'project': { - 'targets': ['RunnerTests'] - } - }; processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + _getMockXcodebuildListProcess(['RunnerTests']), ]; // ... then try to run only integration tests. @@ -1156,19 +1117,47 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('fails if there are no unit tests', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerUITests']), + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1206,19 +1195,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - pluginExampleDirectory - .childDirectory('macos') - .childDirectory('Runner.xcodeproj') - .path, - ], - null), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); }); @@ -1244,6 +1221,15 @@ void main() { final Directory androidFolder = pluginExampleDirectory.childDirectory('android'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // iOS list + MockProcess(), // iOS run + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // macOS list + MockProcess(), // macOS run + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--android', @@ -1266,36 +1252,11 @@ void main() { orderedEquals([ ProcessCall(androidFolder.childFile('gradlew').path, const ['testDebugUnitTest'], androidFolder.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1309,6 +1270,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -1327,20 +1293,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); @@ -1353,6 +1307,11 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', @@ -1371,22 +1330,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'test', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'foo_destination', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), ])); }); @@ -1460,6 +1406,11 @@ void main() { ], ); + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + // Simulate failing Android, but not iOS. final String gradlewPath = pluginDir .childDirectory('example') From 2076d1ddbbcfc6ba149ac214fc1848145b1124eb Mon Sep 17 00:00:00 2001 From: Yaroslav Pronin Date: Tue, 14 Sep 2021 17:27:30 +0300 Subject: [PATCH 028/259] [path_provider_linux] Using TMPDIR env as a primary temporary path (#4218) TMPDIR is a standard variable on UNIX/Linux systems, and is often used in containers such as Flatpak to redirect to a temporary folder inside a sandbox. This allows not to make hard bindings to the /tmp directory Fixes flutter/flutter#87742 --- .../path_provider_linux/CHANGELOG.md | 4 ++++ .../lib/path_provider_linux.dart | 17 +++++++++++++++- .../path_provider_linux/pubspec.yaml | 2 +- .../test/path_provider_linux_test.dart | 20 +++++++++++++++++-- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 66c11a42c3eb..6f18d0d6ae58 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Now `getTemporaryPath` returns the value of the `TMPDIR` environment variable primarily. If `TMPDIR` is not set, `/tmp` is returned. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart index 38800bee1f2e..ab18db69ddfb 100644 --- a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart +++ b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:xdg_directories/xdg_directories.dart' as xdg; @@ -12,6 +13,17 @@ import 'package:xdg_directories/xdg_directories.dart' as xdg; /// /// This class implements the `package:path_provider` functionality for linux class PathProviderLinux extends PathProviderPlatform { + /// Constructs an instance of [PathProviderLinux] + PathProviderLinux() : _environment = Platform.environment; + + /// Constructs an instance of [PathProviderLinux] with the given [environment] + @visibleForTesting + PathProviderLinux.private({ + required Map environment, + }) : _environment = environment; + + final Map _environment; + /// Registers this class as the default instance of [PathProviderPlatform] static void registerWith() { PathProviderPlatform.instance = PathProviderLinux(); @@ -19,7 +31,10 @@ class PathProviderLinux extends PathProviderPlatform { @override Future getTemporaryPath() { - return Future.value('/tmp'); + final String environmentTmpDir = _environment['TMPDIR'] ?? ''; + return Future.value( + environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, + ); } @override diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 4d43302ce6b3..f5b7a88ca232 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.2 +version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart index e058d0d56039..6dd35000a8ea 100644 --- a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart +++ b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart @@ -13,8 +13,24 @@ void main() { expect(PathProviderPlatform.instance, isA()); }); - test('getTemporaryPath', () async { - final PathProviderPlatform plugin = PathProviderPlatform.instance; + test('getTemporaryPath defaults to TMPDIR', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {'TMPDIR': '/run/user/0/tmp'}, + ); + expect(await plugin.getTemporaryPath(), '/run/user/0/tmp'); + }); + + test('getTemporaryPath uses fallback if TMPDIR is empty', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {'TMPDIR': ''}, + ); + expect(await plugin.getTemporaryPath(), '/tmp'); + }); + + test('getTemporaryPath uses fallback if TMPDIR is unset', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {}, + ); expect(await plugin.getTemporaryPath(), '/tmp'); }); From b85edebe7134cdf2f9397a0882ba739b0319cef7 Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Tue, 14 Sep 2021 13:36:53 -0700 Subject: [PATCH 029/259] [ci] use background script (#4349) Uses the background script annotation, since & no longer works. --- .cirrus.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 5b696031b304..ea728267dd4c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -212,8 +212,10 @@ task: - git clone https://github.com/flutter/web_installers.git - cd web_installers/packages/web_drivers/ - dart pub get + chromedriver_background_script: + - cd web_installers/packages/web_drivers/ - dart lib/web_driver_installer.dart chromedriver --install-only - - ./chromedriver/chromedriver --port=4444 & + - ./chromedriver/chromedriver --port=4444 build_script: - ./script/tool_runner.sh build-examples --web drive_script: From 7fff936c6c691d59db35da5750433abeabba8ffa Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 14 Sep 2021 15:09:40 -0700 Subject: [PATCH 030/259] Give the labeler access to PRs (#4348) --- .github/workflows/pull_request_label.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index 16694f4dbf1b..825a3afd8508 100644 --- a/.github/workflows/pull_request_label.yml +++ b/.github/workflows/pull_request_label.yml @@ -14,6 +14,8 @@ on: jobs: label: + permissions: + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 From 41cb992daef6ef7a8470714b8828316b744c4eda Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 15 Sep 2021 00:57:03 -0400 Subject: [PATCH 031/259] Fix crash from `null` Google Maps object (#4250) --- .../google_maps_flutter/google_maps_flutter/CHANGELOG.md | 4 ++++ .../io/flutter/plugins/googlemaps/GoogleMapController.java | 6 +++++- .../flutter/plugins/googlemapsexample/GoogleMapsTest.java | 2 -- .../google_maps_flutter/google_maps_flutter/pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 3080d4a2d733..709aa3b90c19 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.9 + +* Fix Android `NullPointerException` caused by the `GoogleMapController` being disposed before `GoogleMap` was ready. + ## 2.0.8 * Mark iOS arm64 simulators as unsupported. diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 05e016c32e27..056e10631011 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -59,7 +59,7 @@ final class GoogleMapController private final MethodChannel methodChannel; private final GoogleMapOptions options; @Nullable private MapView mapView; - private GoogleMap googleMap; + @Nullable private GoogleMap googleMap; private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private boolean myLocationButtonEnabled = false; @@ -508,6 +508,10 @@ public void dispose() { } private void setGoogleMapListener(@Nullable GoogleMapListener listener) { + if (googleMap == null) { + Log.v(TAG, "Controller was disposed before GoogleMap was ready."); + return; + } googleMap.setOnCameraMoveStartedListener(listener); googleMap.setOnCameraMoveListener(listener); googleMap.setOnCameraIdleListener(listener); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java index 43ddeaae1579..40552ddf7be1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java @@ -8,11 +8,9 @@ import androidx.test.core.app.ActivityScenario; import io.flutter.plugins.googlemaps.GoogleMapsPlugin; -import org.junit.Ignore; import org.junit.Test; public class GoogleMapsTest { - @Ignore("Currently failing: https://github.com/flutter/flutter/issues/87566") @Test public void googleMapsPluginIsAdded() { final ActivityScenario scenario = diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index f1dc21ae2600..9929eb935df1 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/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.8 +version: 2.0.9 environment: sdk: '>=2.12.0 <3.0.0' From 5f1c83e3933e7bc993b011721c330c241fe9796a Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Tue, 14 Sep 2021 22:02:03 -0700 Subject: [PATCH 032/259] Run firebase test in flutter-cirrus (#4332) --- .cirrus.yml | 2 +- script/tool/lib/src/firebase_test_lab_command.dart | 2 +- script/tool/test/firebase_test_lab_command_test.dart | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index ea728267dd4c..0b3f07da48e1 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -159,7 +159,7 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!c9446a7b11d5520c2ebce3c64ccc82fe6d146272cb06a4a4590e22c389f33153f951347a25422522df1a81fe2f085e9a!] build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4fc47c0da70c..941cba3a6945 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -27,7 +27,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { }) : super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( 'project', - defaultsTo: 'flutter-infra', + defaultsTo: 'flutter-cirrus', help: 'The Firebase project name.', ); final String? homeDir = io.Platform.environment['HOME']; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 7716990b323c..e39ccf30b136 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -130,7 +130,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin1/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -207,7 +207,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -433,7 +433,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true'.split(' '), @@ -588,7 +588,7 @@ void main() { .split(' '), null), ProcessCall( - 'gcloud', 'config set project flutter-infra'.split(' '), null), + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), ProcessCall( '/packages/plugin/example/android/gradlew', 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' From d217642a428985b1a6e95a66354a47ebf872fedb Mon Sep 17 00:00:00 2001 From: reez12g Date: Wed, 15 Sep 2021 19:02:04 +0900 Subject: [PATCH 033/259] [image_picker] Update README for image_picker's retrieveLostData() to support multiple files (#4336) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/README.md | 14 ++++---------- packages/image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 5dc260993773..de815b1ff302 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4+1 + +* Fix README Example for `ImagePickerCache` to cache multiple files. + ## 0.8.4 * Update `ImagePickerCache` to cache multiple files. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 7499c356f3aa..fc4813d1cee5 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -62,14 +62,10 @@ Future getLostData() async { if (response.isEmpty) { return; } - if (response.file != null) { - setState(() { - if (response.type == RetrieveType.video) { - _handleVideo(response.file); - } else { - _handleImage(response.file); - } - }); + if (response.files != null) { + for(final XFile file in response.files) { + _handleFile(file); + } } else { _handleError(response.exception); } @@ -78,8 +74,6 @@ Future getLostData() async { There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. -On Android, `retrieveLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634). - ## Migrating to 0.8.2+ Starting with version **0.8.2** of the image_picker plugin, new methods have been added for picking files that return `XFile` instances (from the [cross_file](https://pub.dev/packages/cross_file) package) rather than the plugin's own `PickedFile` instances. While the previous methods still exist, it is already recommended to start migrating over to their new equivalents. Eventually, `PickedFile` and the methods that return instances of it will be deprecated and removed. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 3bbcfe99882e..fca821be2d5f 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/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.4 +version: 0.8.4+1 environment: sdk: ">=2.12.0 <3.0.0" From edc899e7384c8e4e0e5848a717e0a59abe63ac5a Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Wed, 15 Sep 2021 06:49:15 -0700 Subject: [PATCH 034/259] [ci] Grant contents: write permission to release.yml (#4350) --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fbe499f0963..a64acf7692f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,9 @@ jobs: release: if: github.repository_owner == 'flutter' name: release + permissions: + # Release needs to push a tag back to the repo. + contents: write runs-on: ubuntu-latest steps: - name: "Install Flutter" From bb3ebb2b79eb856f641398b7776c52539acfce04 Mon Sep 17 00:00:00 2001 From: Bruno Bowden Date: Wed, 15 Sep 2021 09:47:03 -0700 Subject: [PATCH 035/259] [multiple] Java 8 target for all plugins with -Werror compiler arg (#3216) --- packages/android_alarm_manager/android/build.gradle | 8 ++++---- packages/android_intent/CHANGELOG.md | 1 + packages/android_intent/android/build.gradle | 6 ++++-- packages/camera/camera/android/build.gradle | 4 ++-- packages/connectivity/connectivity/CHANGELOG.md | 1 + .../connectivity/connectivity/android/build.gradle | 4 ++++ packages/path_provider/path_provider/CHANGELOG.md | 3 ++- .../path_provider/path_provider/android/build.gradle | 8 +++----- packages/path_provider/path_provider/pubspec.yaml | 2 +- packages/video_player/video_player/CHANGELOG.md | 4 ++++ .../video_player/video_player/android/build.gradle | 8 +++----- .../video_player/example/android/app/build.gradle | 11 ++++------- packages/video_player/video_player/pubspec.yaml | 2 +- 13 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index b173137786a9..7712ed56fe6f 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -28,10 +28,6 @@ apply plugin: 'com.android.library' android { compileSdkVersion 29 - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -54,6 +50,10 @@ android { } } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 82cd5db3e4e4..79eafe70e821 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove references to the V1 Android embedding. * Updated Android lint settings. +* Specify Java 8 for Android build. ## 2.0.2 diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index e8b9f3810146..f0af1602dbb1 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -37,8 +37,10 @@ android { disable 'InvalidPackage' disable 'GradleDependency' } - - + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 9bbafb653ef8..61d13e5579cc 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -39,8 +39,8 @@ android { baseline file("lint-baseline.xml") } compileOptions { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index f5489692bee9..932565842efd 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove references to the Android V1 embedding. * Updated Android lint settings. +* Specify Java 8 for Android build. ## 3.0.6 diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index 983f29b142de..e1ba0c7c892e 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -50,4 +50,8 @@ android { } } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index ba7bb3dc7ada..f3f3b2e4da12 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.0.4 * Updated Android lint settings. +* Specify Java 8 for Android build. ## 2.0.3 diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index 3458140bd0eb..1a22f135fe5a 100644 --- a/packages/path_provider/path_provider/android/build.gradle +++ b/packages/path_provider/path_provider/android/build.gradle @@ -32,11 +32,9 @@ android { disable 'InvalidPackage' disable 'GradleDependency' } - android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 10352e44c452..febfef516f85 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index a82455231ecd..b0227fe83611 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.1 + +* Specify Java 8 for Android build. + ## 2.2.0 * Add `contentUri` based VideoPlayerController. diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index 9d9984439370..5d6b737f47a5 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -37,11 +37,9 @@ android { disable 'InvalidPackage' disable 'GradleDependency' } - android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } dependencies { diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index fc0673ad1bd8..0d1d5031ef4f 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -30,6 +30,10 @@ android { lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { applicationId "io.flutter.plugins.videoplayerexample" @@ -40,13 +44,6 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } - } - buildTypes { release { // TODO: Add your own signing config for the release build. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 86eee3c9bf42..658357b8fbb1 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.0 +version: 2.2.1 environment: sdk: ">=2.12.0 <3.0.0" From af758d5d643af092c69d79467108d5cf006dab6d Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 16 Sep 2021 01:53:08 +0200 Subject: [PATCH 036/259] [video_player] Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer` (#4344) * fix: do not removeListener if VideoPlayerController is already disposed Co-authored-by: David Iglesias Teixeira --- .../video_player/video_player/CHANGELOG.md | 4 + .../controller_swap_test.dart | 92 +++++++++++++++++++ .../video_player/lib/video_player.dart | 9 ++ .../video_player/video_player/pubspec.yaml | 2 +- 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 packages/video_player/video_player/example/integration_test/controller_swap_test.dart diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index b0227fe83611..134b2dc1cbf5 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.2 + +* Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer`. + ## 2.2.1 * Specify Java 8 for Android build. diff --git a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart new file mode 100644 index 000000000000..cae51767f4aa --- /dev/null +++ b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +const Duration _playDuration = Duration(seconds: 1); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets( + 'can substitute one controller by another without crashing', + (WidgetTester tester) async { + VideoPlayerController controller = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + VideoPlayerController another = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + await controller.initialize(); + await another.initialize(); + await controller.setVolume(0); + await another.setVolume(0); + + final Completer started = Completer(); + final Completer ended = Completer(); + bool startedBuffering = false; + bool endedBuffering = false; + + another.addListener(() { + if (another.value.isBuffering && !startedBuffering) { + startedBuffering = true; + started.complete(); + } + if (startedBuffering && !another.value.isBuffering && !endedBuffering) { + endedBuffering = true; + ended.complete(); + } + }); + + // Inject a widget with `controller`... + await tester.pumpWidget(renderVideoWidget(controller)); + await controller.play(); + await tester.pumpAndSettle(_playDuration); + await controller.pause(); + + // Disposing controller causes the Widget to crash in the next line + // (Issue https://github.com/flutter/flutter/issues/90046) + await controller.dispose(); + + // Now replace it with `another` controller... + await tester.pumpWidget(renderVideoWidget(another)); + await another.play(); + await another.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await another.pause(); + + // Expect that `another` played. + expect(another.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await started; + expect(startedBuffering, true); + + await ended; + expect(endedBuffering, true); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); +} + +Widget renderVideoWidget(VideoPlayerController controller) { + return Material( + elevation: 0, + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: AspectRatio( + key: Key('same'), + aspectRatio: controller.value.aspectRatio, + child: VideoPlayer(controller), + ), + ), + ), + ); +} diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 685563ae12c3..0d682c9b18e4 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -594,6 +594,15 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith(caption: _getCaptionAt(position)); } + @override + void removeListener(VoidCallback listener) { + // Prevent VideoPlayer from causing an exception to be thrown when attempting to + // remove its own listener after the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; } diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 658357b8fbb1..2fa5d663d3c7 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.1 +version: 2.2.2 environment: sdk: ">=2.12.0 <3.0.0" From 5d64092352e885c82a306cd9c04672fe31e2640c Mon Sep 17 00:00:00 2001 From: Koen Van Looveren Date: Thu, 16 Sep 2021 03:02:07 +0200 Subject: [PATCH 037/259] [video_player] bugfix caption still showing when text is empty (#3374) --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ .../video_player/lib/video_player.dart | 14 ++++++++------ packages/video_player/video_player/pubspec.yaml | 2 +- .../video_player/test/video_player_test.dart | 5 +++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 134b2dc1cbf5..69ad92c13fe8 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.3 + +* Fixed empty caption text still showing the caption widget. + ## 2.2.2 * Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer`. diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 0d682c9b18e4..fe3437593a81 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -946,11 +946,12 @@ class ClosedCaption extends StatelessWidget { /// Creates a a new closed caption, designed to be used with /// [VideoPlayerValue.caption]. /// - /// If [text] is null, nothing will be displayed. + /// If [text] is null or empty, nothing will be displayed. const ClosedCaption({Key? key, this.text, this.textStyle}) : super(key: key); /// The text that will be shown in the closed caption, or null if no caption /// should be shown. + /// If the text is empty the caption will not be shown. final String? text; /// Specifies how the text in the closed caption should look. @@ -961,16 +962,17 @@ class ClosedCaption extends StatelessWidget { @override Widget build(BuildContext context) { + final text = this.text; + if (text == null || text.isEmpty) { + return SizedBox.shrink(); + } + final TextStyle effectiveTextStyle = textStyle ?? DefaultTextStyle.of(context).style.copyWith( fontSize: 36.0, color: Colors.white, ); - if (text == null) { - return SizedBox.shrink(); - } - return Align( alignment: Alignment.bottomCenter, child: Padding( @@ -982,7 +984,7 @@ class ClosedCaption extends StatelessWidget { ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Text(text!, style: effectiveTextStyle), + child: Text(text, style: effectiveTextStyle), ), ), ), diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 2fa5d663d3c7..317d46d851b7 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.2 +version: 2.2.3 environment: sdk: ">=2.12.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 5fdc1fbf4574..a929c6827fd0 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -162,6 +162,11 @@ void main() { expect(find.byType(Text), findsNothing); }); + testWidgets('handles empty text', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: ''))); + expect(find.byType(Text), findsNothing); + }); + testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { final String text = 'foo'; From c13218faba5168ef9067502b3e176a600bf4bf5a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 16 Sep 2021 03:52:06 +0200 Subject: [PATCH 038/259] [camera_web] Release the camera stream used to request video and audio permissions (#4342) --- packages/camera/camera_web/CHANGELOG.md | 4 +++ packages/camera/camera_web/README.md | 2 +- .../integration_test/camera_web_test.dart | 27 +++++++++++++++++++ .../camera/camera_web/lib/src/camera_web.dart | 5 +++- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index b116636c2808..098fe62c3a1f 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fix cameraNotReadable error that prevented access to the camera on some Android devices. + ## 0.2.0 * Initial release, adapted from the Flutter [I/O Photobooth](https://photobooth.flutter.dev/) project. diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index c6e1e0f13cab..032a345fad99 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -9,7 +9,7 @@ The web implementation of [`camera`][camera]. ### Depend on the package This package is not an [endorsed implementation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin) -of the google_maps_flutter plugin yet, so you'll need to [add it explicitly](https://pub.dev/packages/camera_web/install). +of the camera plugin yet, so you'll need to [add it explicitly](https://pub.dev/packages/camera_web/install). ## Example diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 4bc10badab05..f469f3c30849 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -110,6 +110,33 @@ void main() { ).called(1); }); + testWidgets( + 'releases the camera stream ' + 'used to request video and audio permissions', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + var videoTrackStopped = false; + when(videoTrack.stop).thenAnswer((_) { + videoTrackStopped = true; + }); + + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), + ), + ), + ).thenAnswer( + (_) => Future.value( + FakeMediaStream([videoTrack]), + ), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + expect(videoTrackStopped, isTrue); + }); + testWidgets( 'gets a video stream ' 'for a video input device', (tester) async { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 5c976b8f8657..92c43c45b6b9 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -89,12 +89,15 @@ class CameraPlugin extends CameraPlatform { } // Request video and audio permissions. - await _cameraService.getMediaStreamForOptions( + final cameraStream = await _cameraService.getMediaStreamForOptions( CameraOptions( audio: AudioConstraints(enabled: true), ), ); + // Release the camera stream used to request video and audio permissions. + cameraStream.getVideoTracks().forEach((videoTrack) => videoTrack.stop()); + // Request available media devices. final devices = await mediaDevices.enumerateDevices(); From 8009a2bb12de53d105c019b052df5bd6ff06b3c3 Mon Sep 17 00:00:00 2001 From: Tim Sneath Date: Thu, 16 Sep 2021 09:57:06 -0700 Subject: [PATCH 039/259] Uncomment Marker icons now that ImageListener API change has landed in stable (#2443) --- .../example/lib/place_marker.dart | 95 +++++++++---------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 4e9f4c14ebd0..3d083e5f9fa9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -245,42 +246,36 @@ class PlaceMarkerBodyState extends State { }); } -// A breaking change to the ImageStreamListener API affects this sample. -// I've updates the sample to use the new API, but as we cannot use the new -// API before it makes it to stable I'm commenting out this sample for now -// TODO(amirh): uncomment this one the ImageStream API change makes it to stable. -// https://github.com/flutter/flutter/issues/33438 -// -// void _setMarkerIcon(BitmapDescriptor assetIcon) { -// if (selectedMarker == null) { -// return; -// } -// -// final Marker marker = markers[selectedMarker]; -// setState(() { -// markers[selectedMarker] = marker.copyWith( -// iconParam: assetIcon, -// ); -// }); -// } -// -// Future _getAssetIcon(BuildContext context) async { -// final Completer bitmapIcon = -// Completer(); -// final ImageConfiguration config = createLocalImageConfiguration(context); -// -// const AssetImage('assets/red_square.png') -// .resolve(config) -// .addListener(ImageStreamListener((ImageInfo image, bool sync) async { -// final ByteData bytes = -// await image.image.toByteData(format: ImageByteFormat.png); -// final BitmapDescriptor bitmap = -// BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); -// bitmapIcon.complete(bitmap); -// })); -// -// return await bitmapIcon.future; -// } + void _setMarkerIcon(MarkerId markerId, BitmapDescriptor assetIcon) { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + iconParam: assetIcon, + ); + }); + } + + Future _getAssetIcon(BuildContext context) async { + final Completer bitmapIcon = + Completer(); + final ImageConfiguration config = createLocalImageConfiguration(context); + + const AssetImage('assets/red_square.png') + .resolve(config) + .addListener(ImageStreamListener((ImageInfo image, bool sync) async { + final ByteData? bytes = + await image.image.toByteData(format: ImageByteFormat.png); + if (bytes == null) { + bitmapIcon.completeError(Exception('Unable to encode icon')); + return; + } + final BitmapDescriptor bitmap = + BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); + bitmapIcon.complete(bitmap); + })); + + return await bitmapIcon.future; + } @override Widget build(BuildContext context) { @@ -386,22 +381,18 @@ class PlaceMarkerBodyState extends State { ? null : () => _changeZIndex(selectedId), ), - // A breaking change to the ImageStreamListener API affects this sample. - // I've updates the sample to use the new API, but as we cannot use the new - // API before it makes it to stable I'm commenting out this sample for now - // TODO(amirh): uncomment this one the ImageStream API change makes it to stable. - // https://github.com/flutter/flutter/issues/33438 - // - // TextButton( - // child: const Text('set marker icon'), - // onPressed: () { - // _getAssetIcon(context).then( - // (BitmapDescriptor icon) { - // _setMarkerIcon(icon); - // }, - // ); - // }, - // ), + TextButton( + child: const Text('set marker icon'), + onPressed: selectedId == null + ? null + : () { + _getAssetIcon(context).then( + (BitmapDescriptor icon) { + _setMarkerIcon(selectedId, icon); + }, + ); + }, + ), ], ), ], From aa69736c460c60899eeb1dbddc573a929198f7bd Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Thu, 16 Sep 2021 23:06:55 +0200 Subject: [PATCH 040/259] [camera_web] Add video recording functionality (#4210) * feat: Add Support for Video Recording in Camera Web * docs: add video recording documentation Co-authored-by: Bartosz Selwesiuk --- .../camera/lib/src/camera_controller.dart | 2 +- packages/camera/camera_web/CHANGELOG.md | 3 +- packages/camera/camera_web/README.md | 17 +- .../camera_error_code_test.dart | 7 + .../example/integration_test/camera_test.dart | 692 +++++++++++++++++- .../integration_test/camera_web_test.dart | 623 +++++++++++++++- .../integration_test/helpers/mocks.dart | 30 + .../camera/camera_web/lib/src/camera.dart | 258 ++++++- .../camera/camera_web/lib/src/camera_web.dart | 65 +- .../lib/src/types/camera_error_code.dart | 4 + packages/camera/camera_web/pubspec.yaml | 2 +- 11 files changed, 1622 insertions(+), 81 deletions(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index f21a3b12c81f..8cf1e90e36c1 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -335,7 +335,7 @@ class CameraController extends ValueNotifier { /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. /// If video recording is intended, calling this early eliminates this delay /// that would otherwise be experienced when video recording is started. - /// This operation is a no-op on Android. + /// This operation is a no-op on Android and Web. /// /// Throws a [CameraException] if the prepare fails. Future prepareForVideoRecording() async { diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 098fe62c3a1f..8596b3595852 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.1 +* Add video recording functionality. * Fix cameraNotReadable error that prevented access to the camera on some Android devices. ## 0.2.0 diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index 032a345fad99..918e695496a4 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -83,11 +83,24 @@ if (kIsWeb) { } ``` +### Video recording + +The video recording implementation is backed by [MediaRecorder Web API](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) with the following [browser support](https://caniuse.com/mdn-api_mediarecorder): + +![Data on support for the MediaRecorder feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/mediarecorder.png). + +A video is recorded in one of the following video MIME types: +- video/webm (e.g. on Chrome or Firefox) +- video/mp4 (e.g. on Safari) + +Pausing, resuming or stopping the video recording throws a `PlatformException` with the `videoRecordingNotStarted` error code if the video recording was not started. + +For the browsers that do not support the video recording: +- `CameraPlatform.startVideoRecording` throws a `PlatformException` with the `notSupported` error code. + ## Missing implementation The web implementation of [`camera`][camera] is missing the following features: - -- Video recording ([in progress](https://github.com/flutter/plugins/pull/4210)) - Exposure mode, point and offset - Focus mode and point - Sensor orientation diff --git a/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart index d0250c6e4e26..a298b57dfd7f 100644 --- a/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart @@ -113,6 +113,13 @@ void main() { ); }); + testWidgets('videoRecordingNotStarted', (tester) async { + expect( + CameraErrorCode.videoRecordingNotStarted.toString(), + equals('videoRecordingNotStarted'), + ); + }); + testWidgets('unknown', (tester) async { expect( CameraErrorCode.unknown.toString(), diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 712d8c77ff3e..3a25e33c5398 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:html'; import 'dart:ui'; @@ -531,7 +532,7 @@ void main() { ).called(1); }); - group('throws CameraWebException', () { + group('throws a CameraWebException', () { testWidgets( 'with torchModeNotSupported error ' 'when there are no media devices', (tester) async { @@ -774,7 +775,7 @@ void main() { ).called(1); }); - group('throws CameraWebException', () { + group('throws a CameraWebException', () { testWidgets( 'with zoomLevelInvalid error ' 'when the provided zoom level is below minimum', (tester) async { @@ -827,20 +828,21 @@ void main() { .thenReturn(zoomLevelCapability); expect( - () => camera.setZoomLevel(105.0), - throwsA( - isA() - .having( - (e) => e.cameraId, - 'cameraId', - textureId, - ) - .having( - (e) => e.code, - 'code', - CameraErrorCode.zoomLevelInvalid, - ), - )); + () => camera.setZoomLevel(105.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + ), + ); }); }); }); @@ -943,6 +945,503 @@ void main() { }); }); + group('video recording', () { + const supportedVideoType = 'video/webm'; + + late MediaRecorder mediaRecorder; + + bool isVideoTypeSupported(String type) => type == supportedVideoType; + + setUp(() { + mediaRecorder = MockMediaRecorder(); + + when(() => mediaRecorder.onError) + .thenAnswer((_) => const Stream.empty()); + }); + + group('startVideoRecording', () { + testWidgets( + 'creates a media recorder ' + 'with appropriate options', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + expect( + camera.mediaRecorder!.stream, + equals(camera.stream), + ); + + expect( + camera.mediaRecorder!.mimeType, + equals(supportedVideoType), + ); + + expect( + camera.mediaRecorder!.state, + equals('recording'), + ); + }); + + testWidgets('listens to the media recorder data events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).called(1); + }); + + testWidgets('listens to the media recorder stop events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify( + () => mediaRecorder.addEventListener('stop', any()), + ).called(1); + }); + + testWidgets('starts a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify(mediaRecorder.start).called(1); + }); + + testWidgets( + 'starts a video recording ' + 'with maxVideoDuration', (tester) async { + const maxVideoDuration = Duration(hours: 1); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + verify(() => mediaRecorder.start(maxVideoDuration.inMilliseconds)) + .called(1); + }); + + group('throws a CameraWebException', () { + testWidgets( + 'with notSupported error ' + 'when maxVideoDuration is 0 milliseconds or less', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + expect( + () => camera.startVideoRecording(maxVideoDuration: Duration.zero), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported, + ), + ), + ); + }); + + testWidgets( + 'with notSupported error ' + 'when no video types are supported', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..isVideoTypeSupported = (type) => false; + + await camera.initialize(); + await camera.play(); + + expect( + camera.startVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported, + ), + ), + ); + }); + }); + }); + + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..mediaRecorder = mediaRecorder; + + await camera.pauseVideoRecording(); + + verify(mediaRecorder.pause).called(1); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.pauseVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..mediaRecorder = mediaRecorder; + + await camera.resumeVideoRecording(); + + verify(mediaRecorder.resume).called(1); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.resumeVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('stopVideoRecording', () { + testWidgets( + 'stops a video recording and ' + 'returns the captured file ' + 'based on all video data parts', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + late void Function(Event) videoDataAvailableListener; + late void Function(Event) videoRecordingStoppedListener; + + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + + Blob? finalVideo; + List? videoParts; + camera.blobBuilder = (blobs, videoType) { + videoParts = [...blobs]; + finalVideo = Blob(blobs, videoType); + return finalVideo!; + }; + + await camera.startVideoRecording(); + final videoFileFuture = camera.stopVideoRecording(); + + final capturedVideoPartOne = Blob([]); + final capturedVideoPartTwo = Blob([]); + + final capturedVideoParts = [ + capturedVideoPartOne, + capturedVideoPartTwo, + ]; + + videoDataAvailableListener + ..call(FakeBlobEvent(capturedVideoPartOne)) + ..call(FakeBlobEvent(capturedVideoPartTwo)); + + videoRecordingStoppedListener.call(Event('stop')); + + final videoFile = await videoFileFuture; + + verify(mediaRecorder.stop).called(1); + + expect( + videoFile, + isNotNull, + ); + + expect( + videoFile.mimeType, + equals(supportedVideoType), + ); + + expect( + videoFile.name, + equals(finalVideo.hashCode.toString()), + ); + + expect( + videoParts, + equals(capturedVideoParts), + ); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.stopVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('on video data available', () { + late void Function(Event) videoDataAvailableListener; + + setUp(() { + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + }); + + testWidgets( + 'stops a video recording ' + 'if maxVideoDuration is given and ' + 'the recording was not stopped manually', (tester) async { + const maxVideoDuration = Duration(hours: 1); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + when(() => mediaRecorder.state).thenReturn('recording'); + + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + + await Future.microtask(() {}); + + verify(mediaRecorder.stop).called(1); + }); + }); + + group('on video recording stopped', () { + late void Function(Event) videoRecordingStoppedListener; + + setUp(() { + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + }); + + testWidgets('stops listening to the media recorder data events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + verify( + () => mediaRecorder.removeEventListener('dataavailable', any()), + ).called(1); + }); + + testWidgets('stops listening to the media recorder stop events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + verify( + () => mediaRecorder.removeEventListener('stop', any()), + ).called(1); + }); + + testWidgets('stops listening to the media recorder errors', + (tester) async { + final onErrorStreamController = StreamController(); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + when(() => mediaRecorder.onError) + .thenAnswer((_) => onErrorStreamController.stream); + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + expect( + onErrorStreamController.hasListener, + isFalse, + ); + }); + }); + }); + group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( @@ -951,14 +1450,143 @@ void main() { ); await camera.initialize(); - await camera.dispose(); expect(camera.videoElement.srcObject, isNull); }); + + testWidgets('closes the onEnded stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.onEndedController.isClosed, + isTrue, + ); + }); + + testWidgets('closes the onVideoRecordedEvent stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.videoRecorderController.isClosed, + isTrue, + ); + }); + + testWidgets('closes the onVideoRecordingError stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.videoRecordingErrorController.isClosed, + isTrue, + ); + }); }); group('events', () { + group('onVideoRecordedEvent', () { + testWidgets( + 'emits a VideoRecordedEvent ' + 'when a video recording is created', (tester) async { + const maxVideoDuration = Duration(hours: 1); + const supportedVideoType = 'video/webm'; + + final mediaRecorder = MockMediaRecorder(); + when(() => mediaRecorder.onError) + .thenAnswer((_) => const Stream.empty()); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = (type) => type == 'video/webm'; + + await camera.initialize(); + await camera.play(); + + late void Function(Event) videoDataAvailableListener; + late void Function(Event) videoRecordingStoppedListener; + + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + + final streamQueue = StreamQueue(camera.onVideoRecordedEvent); + + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + Blob? finalVideo; + camera.blobBuilder = (blobs, videoType) { + finalVideo = Blob(blobs, videoType); + return finalVideo!; + }; + + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + videoRecordingStoppedListener.call(Event('stop')); + + expect( + await streamQueue.next, + equals( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.file, + 'file', + isA() + .having( + (f) => f.mimeType, + 'mimeType', + supportedVideoType, + ) + .having( + (f) => f.name, + 'name', + finalVideo.hashCode.toString(), + ), + ) + .having( + (e) => e.maxVideoDuration, + 'maxVideoDuration', + maxVideoDuration, + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + group('onEnded', () { testWidgets( 'emits the default video track ' @@ -1009,22 +1637,40 @@ void main() { await streamQueue.cancel(); }); + }); + group('onVideoRecordingError', () { testWidgets( - 'no longer emits the default video track ' - 'when the camera is disposed', (tester) async { + 'emits an ErrorEvent ' + 'when the media recorder fails ' + 'when recording a video', (tester) async { + final mediaRecorder = MockMediaRecorder(); + final errorController = StreamController(); + final camera = Camera( textureId: textureId, cameraService: cameraService, - ); + )..mediaRecorder = mediaRecorder; + + when(() => mediaRecorder.onError) + .thenAnswer((_) => errorController.stream); + + final streamQueue = StreamQueue(camera.onVideoRecordingError); await camera.initialize(); - await camera.dispose(); + await camera.play(); + + await camera.startVideoRecording(); + + final errorEvent = ErrorEvent('type'); + errorController.add(errorEvent); expect( - camera.onEndedStreamController.isClosed, - isTrue, + await streamQueue.next, + equals(errorEvent), ); + + await streamQueue.cancel(); }); }); }); diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index f469f3c30849..9749559ed8c6 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1019,43 +1019,377 @@ void main() { }); }); - testWidgets('prepareForVideoRecording throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.prepareForVideoRecording(), - throwsUnimplementedError, - ); - }); + group('startVideoRecording', () { + late Camera camera; - testWidgets('startVideoRecording throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsUnimplementedError, - ); + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording).thenAnswer((_) async {}); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('starts a video recording', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + verify(camera.startVideoRecording).called(1); + }); + + testWidgets('listens to the onVideoRecordingError stream', + (tester) async { + final videoRecordingErrorController = StreamController(); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + expect( + videoRecordingErrorController.hasListener, + isTrue, + ); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws DomException', + (tester) async { + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.startVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws CameraWebException', + (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.startVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('stopVideoRecording throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsUnimplementedError, - ); + group('stopVideoRecording', () { + testWidgets('stops a video recording', (tester) async { + final camera = MockCamera(); + final capturedVideo = MockXFile(); + + when(camera.stopVideoRecording) + .thenAnswer((_) => Future.value(capturedVideo)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final video = + await CameraPlatform.instance.stopVideoRecording(cameraId); + + verify(camera.stopVideoRecording).called(1); + + expect(video, capturedVideo); + }); + + testWidgets('stops listening to the onVideoRecordingError stream', + (tester) async { + final camera = MockCamera(); + final videoRecordingErrorController = StreamController(); + + when(camera.startVideoRecording).thenAnswer((_) async => {}); + + when(camera.stopVideoRecording) + .thenAnswer((_) => Future.value(MockXFile())); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + final _ = await CameraPlatform.instance.stopVideoRecording(cameraId); + + expect( + videoRecordingErrorController.hasListener, + isFalse, + ); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.stopVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.stopVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('pauseVideoRecording throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsUnimplementedError, - ); + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (tester) async { + final camera = MockCamera(); + + when(camera.pauseVideoRecording).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pauseVideoRecording(cameraId); + + verify(camera.pauseVideoRecording).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.pauseVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.pauseVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('resumeVideoRecording throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsUnimplementedError, - ); + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (tester) async { + final camera = MockCamera(); + + when(camera.resumeVideoRecording).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumeVideoRecording(cameraId); + + verify(camera.resumeVideoRecording).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.resumeVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.resumeVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); group('setFlashMode', () { @@ -1676,6 +2010,7 @@ void main() { late StreamController errorStreamController, abortStreamController; late StreamController endedStreamController; + late StreamController videoRecordingErrorController; setUp(() { camera = MockCamera(); @@ -1684,6 +2019,7 @@ void main() { errorStreamController = StreamController(); abortStreamController = StreamController(); endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); @@ -1698,6 +2034,11 @@ void main() { when(() => camera.onEnded) .thenAnswer((_) => endedStreamController.stream); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording).thenAnswer((_) async {}); }); testWidgets('disposes the correct camera', (tester) async { @@ -1754,6 +2095,18 @@ void main() { expect(endedStreamController.hasListener, isFalse); }); + testWidgets('cancels the camera video recording error subscriptions', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + group('throws PlatformException', () { testWidgets( 'with notFound error ' @@ -1832,6 +2185,7 @@ void main() { late StreamController errorStreamController, abortStreamController; late StreamController endedStreamController; + late StreamController videoRecordingErrorController; setUp(() { camera = MockCamera(); @@ -1840,6 +2194,7 @@ void main() { errorStreamController = StreamController(); abortStreamController = StreamController(); endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); when(camera.getVideoSize).thenReturn(Size(10, 10)); when(camera.initialize).thenAnswer((_) => Future.value()); @@ -1853,6 +2208,11 @@ void main() { when(() => camera.onEnded) .thenAnswer((_) => endedStreamController.stream); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + when(() => camera.startVideoRecording()).thenAnswer((_) async => {}); }); testWidgets( @@ -2258,13 +2618,210 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on startVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => const Stream.empty()); + + when( + () => camera.startVideoRecording( + maxVideoDuration: any(named: 'maxVideoDuration'), + ), + ).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on the camera video recording error event', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + + final errorEvent = FakeErrorEvent('type', 'message'); + + videoRecordingErrorController.add(errorEvent); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on stopVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.stopVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on pauseVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.pauseVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on resumeVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.resumeVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); - testWidgets('onVideoRecordedEvent throws UnimplementedError', + testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', (tester) async { + final camera = MockCamera(); + final capturedVideo = MockXFile(); + final stream = Stream.value( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)); + when(() => camera.onVideoRecordedEvent).thenAnswer((_) => stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final streamQueue = + StreamQueue(CameraPlatform.instance.onVideoRecordedEvent(cameraId)); + expect( - () => CameraPlatform.instance.onVideoRecordedEvent(cameraId), - throwsUnimplementedError, + await streamQueue.next, + equals( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), + ), ); }); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index e6a11cc0b454..77e9077356f7 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -41,6 +41,8 @@ class MockXFile extends Mock implements XFile {} class MockJsUtil extends Mock implements JsUtil {} +class MockMediaRecorder extends Mock implements MediaRecorder {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); @@ -122,6 +124,34 @@ class FakeElementStream extends Fake } } +/// A fake [BlobEvent] that returns the provided blob [data]. +class FakeBlobEvent extends Fake implements BlobEvent { + FakeBlobEvent(this._blob); + + final Blob? _blob; + + @override + Blob? get data => _blob; +} + +/// A fake [DomException] that returns the provided error [_name] and [_message]. +class FakeErrorEvent extends Fake implements ErrorEvent { + FakeErrorEvent( + String type, [ + String? message, + ]) : _type = type, + _message = message; + + final String _type; + final String? _message; + + @override + String get type => _type; + + @override + String? get message => _message; +} + /// Returns a video element with a blank stream of size [videoSize]. /// /// Can be used to mock a video stream: diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 4b7a185b90f7..cf0187057188 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -26,8 +26,10 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// the video element in [_applyDefaultVideoStyles]. /// See: https://github.com/flutter/flutter/issues/79519 /// -/// The camera can be played/stopped by calling [play]/[stop] -/// or may capture a picture by calling [takePicture]. +/// The camera stream can be played/stopped by calling [play]/[stop], +/// may capture a picture by calling [takePicture] or capture a video +/// by calling [startVideoRecording], [pauseVideoRecording], +/// [resumeVideoRecording] or [stopVideoRecording]. /// /// The camera zoom may be adjusted with [setZoomLevel]. The provided /// zoom level must be a value in the range of [getMinZoomLevel] to @@ -76,15 +78,31 @@ class Camera { /// /// MediaStreamTrack.onended: /// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/onended - Stream get onEnded => onEndedStreamController.stream; + Stream get onEnded => onEndedController.stream; /// The stream controller for the [onEnded] stream. @visibleForTesting - final onEndedStreamController = - StreamController.broadcast(); + final onEndedController = StreamController.broadcast(); StreamSubscription? _onEndedSubscription; + /// The stream of the camera video recording errors. + /// + /// This occurs when the video recording is not allowed or an unsupported + /// codec is used. + /// + /// MediaRecorder.error: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/error_event + Stream get onVideoRecordingError => + videoRecordingErrorController.stream; + + /// The stream controller for the [onVideoRecordingError] stream. + @visibleForTesting + final videoRecordingErrorController = + StreamController.broadcast(); + + StreamSubscription? _onVideoRecordingErrorSubscription; + /// The camera flash mode. @visibleForTesting FlashMode? flashMode; @@ -96,6 +114,41 @@ class Camera { @visibleForTesting html.Window? window = html.window; + /// The recorder used to record a video from the camera. + @visibleForTesting + html.MediaRecorder? mediaRecorder; + + /// Whether the video of the given type is supported. + @visibleForTesting + bool Function(String) isVideoTypeSupported = + html.MediaRecorder.isTypeSupported; + + /// The list of consecutive video data files recorded with [mediaRecorder]. + List _videoData = []; + + /// Completes when the video recording is stopped/finished. + Completer? _videoAvailableCompleter; + + /// A data listener fired when a new part of video data is available. + void Function(html.Event)? _videoDataAvailableListener; + + /// A listener fired when a video recording is stopped. + void Function(html.Event)? _videoRecordingStoppedListener; + + /// A builder to merge a list of blobs into a single blob. + @visibleForTesting + html.Blob Function(List blobs, String type) blobBuilder = + (blobs, type) => html.Blob(blobs, type); + + /// The stream that emits a [VideoRecordedEvent] when a video recording is created. + Stream get onVideoRecordedEvent => + videoRecorderController.stream; + + /// The stream controller for the [onVideoRecordedEvent] stream. + @visibleForTesting + final StreamController videoRecorderController = + StreamController.broadcast(); + /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. /// Emits the camera default video track on the [onEnded] stream when it ends. @@ -130,7 +183,7 @@ class Camera { final defaultVideoTrack = videoTracks.first; _onEndedSubscription = defaultVideoTrack.onEnded.listen((html.Event _) { - onEndedStreamController.add(defaultVideoTrack); + onEndedController.add(defaultVideoTrack); }); } } @@ -158,7 +211,7 @@ class Camera { void stop() { final videoTracks = stream!.getVideoTracks(); if (videoTracks.isNotEmpty) { - onEndedStreamController.add(videoTracks.first); + onEndedController.add(videoTracks.first); } final tracks = stream?.getTracks(); @@ -365,23 +418,204 @@ class Camera { /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); - /// Disposes the camera by stopping the camera stream - /// and reloading the camera source. + /// Starts a new video recording using [html.MediaRecorder]. + /// + /// Throws a [CameraWebException] if the provided maximum video duration is invalid + /// or the browser does not support any of the available video mime types + /// from [_videoMimeType]. + Future startVideoRecording({Duration? maxVideoDuration}) async { + if (maxVideoDuration != null && maxVideoDuration.inMilliseconds <= 0) { + throw CameraWebException( + textureId, + CameraErrorCode.notSupported, + 'The maximum video duration must be greater than 0 milliseconds.', + ); + } + + mediaRecorder ??= html.MediaRecorder(videoElement.srcObject!, { + 'mimeType': _videoMimeType, + }); + + _videoAvailableCompleter = Completer(); + + _videoDataAvailableListener = + (event) => _onVideoDataAvailable(event, maxVideoDuration); + + _videoRecordingStoppedListener = + (event) => _onVideoRecordingStopped(event, maxVideoDuration); + + mediaRecorder!.addEventListener( + 'dataavailable', + _videoDataAvailableListener, + ); + + mediaRecorder!.addEventListener( + 'stop', + _videoRecordingStoppedListener, + ); + + _onVideoRecordingErrorSubscription = + mediaRecorder!.onError.listen((html.Event event) { + final error = event as html.ErrorEvent; + if (error != null) { + videoRecordingErrorController.add(error); + } + }); + + if (maxVideoDuration != null) { + mediaRecorder!.start(maxVideoDuration.inMilliseconds); + } else { + // Don't pass the null duration as that will fire a `dataavailable` event directly. + mediaRecorder!.start(); + } + } + + void _onVideoDataAvailable( + html.Event event, [ + Duration? maxVideoDuration, + ]) { + final blob = (event as html.BlobEvent).data; + + // Append the recorded part of the video to the list of all video data files. + if (blob != null) { + _videoData.add(blob); + } + + // Stop the recorder if the video has a maxVideoDuration + // and the recording was not stopped manually. + if (maxVideoDuration != null && mediaRecorder!.state == 'recording') { + mediaRecorder!.stop(); + } + } + + Future _onVideoRecordingStopped( + html.Event event, [ + Duration? maxVideoDuration, + ]) async { + if (_videoData.isNotEmpty) { + // Concatenate all video data files into a single blob. + final videoType = _videoData.first.type; + final videoBlob = blobBuilder(_videoData, videoType); + + // Create a file containing the video blob. + final file = XFile( + html.Url.createObjectUrl(videoBlob), + mimeType: _videoMimeType, + name: videoBlob.hashCode.toString(), + ); + + // Emit an event containing the recorded video file. + videoRecorderController.add( + VideoRecordedEvent(this.textureId, file, maxVideoDuration), + ); + + _videoAvailableCompleter?.complete(file); + } + + // Clean up the media recorder with its event listeners and video data. + mediaRecorder!.removeEventListener( + 'dataavailable', + _videoDataAvailableListener, + ); + + mediaRecorder!.removeEventListener( + 'stop', + _videoDataAvailableListener, + ); + + await _onVideoRecordingErrorSubscription?.cancel(); + + mediaRecorder = null; + _videoDataAvailableListener = null; + _videoRecordingStoppedListener = null; + _videoData.clear(); + } + + /// Pauses the current video recording. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future pauseVideoRecording() async { + if (mediaRecorder == null) { + throw _videoRecordingNotStartedException; + } + mediaRecorder!.pause(); + } + + /// Resumes the current video recording. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future resumeVideoRecording() async { + if (mediaRecorder == null) { + throw _videoRecordingNotStartedException; + } + mediaRecorder!.resume(); + } + + /// Stops the video recording and returns the captured video file. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future stopVideoRecording() async { + if (mediaRecorder == null || _videoAvailableCompleter == null) { + throw _videoRecordingNotStartedException; + } + + mediaRecorder!.stop(); + + return _videoAvailableCompleter!.future; + } + + /// Disposes the camera by stopping the camera stream, + /// the video recording and reloading the camera source. Future dispose() async { - /// Stop the camera stream. + // Stop the camera stream. stop(); - /// Reset the [videoElement] to its initial state. + await videoRecorderController.close(); + mediaRecorder = null; + _videoDataAvailableListener = null; + + // Reset the [videoElement] to its initial state. videoElement ..srcObject = null ..load(); await _onEndedSubscription?.cancel(); _onEndedSubscription = null; + await onEndedController.close(); - await onEndedStreamController.close(); + await _onVideoRecordingErrorSubscription?.cancel(); + _onVideoRecordingErrorSubscription = null; + await videoRecordingErrorController.close(); + } + + /// Returns the first supported video mime type (amongst mp4 and webm) + /// to use when recording a video. + /// + /// Throws a [CameraWebException] if the browser does not support + /// any of the available video mime types. + String get _videoMimeType { + const types = [ + 'video/mp4', + 'video/webm', + ]; + + return types.firstWhere( + (type) => isVideoTypeSupported(type), + orElse: () => throw CameraWebException( + textureId, + CameraErrorCode.notSupported, + 'The browser does not support any of the following video types: ${types.join(',')}.', + ), + ); } + CameraWebException get _videoRecordingNotStartedException => + CameraWebException( + textureId, + CameraErrorCode.videoRecordingNotStarted, + 'The video recorder is uninitialized. The recording might not have been started. Make sure to call `startVideoRecording` first.', + ); + /// Applies default styles to the video [element]. void _applyDefaultVideoStyles(html.VideoElement element) { final isBackCamera = getLensDirection() == CameraLensDirection.back; diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 92c43c45b6b9..0021ee47cbde 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -64,6 +64,9 @@ class CameraPlugin extends CameraPlatform { final _cameraEndedSubscriptions = >{}; + final _cameraVideoRecordingErrorSubscriptions = + >{}; + /// Returns a stream of camera events for the given [cameraId]. Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream @@ -338,7 +341,7 @@ class CameraPlugin extends CameraPlatform { @override Stream onVideoRecordedEvent(int cameraId) { - throw UnimplementedError('onVideoRecordedEvent() is not implemented.'); + return getCamera(cameraId).onVideoRecordedEvent; } @override @@ -422,28 +425,73 @@ class CameraPlugin extends CameraPlatform { } @override - Future prepareForVideoRecording() { - throw UnimplementedError('prepareForVideoRecording() is not implemented.'); + Future prepareForVideoRecording() async { + // This is a no-op as it is not required for the web. } @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { - throw UnimplementedError('startVideoRecording() is not implemented.'); + try { + final camera = getCamera(cameraId); + + // Add camera's video recording errors to the camera events stream. + // The error event fires when the video recording is not allowed or an unsupported + // codec is used. + _cameraVideoRecordingErrorSubscriptions[cameraId] = + camera.onVideoRecordingError.listen((html.ErrorEvent errorEvent) { + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ); + }); + + return camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override - Future stopVideoRecording(int cameraId) { - throw UnimplementedError('stopVideoRecording() is not implemented.'); + Future stopVideoRecording(int cameraId) async { + try { + final videoRecording = await getCamera(cameraId).stopVideoRecording(); + await _cameraVideoRecordingErrorSubscriptions[cameraId]?.cancel(); + return videoRecording; + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override Future pauseVideoRecording(int cameraId) { - throw UnimplementedError('pauseVideoRecording() is not implemented.'); + try { + return getCamera(cameraId).pauseVideoRecording(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override Future resumeVideoRecording(int cameraId) { - throw UnimplementedError('resumeVideoRecording() is not implemented.'); + try { + return getCamera(cameraId).resumeVideoRecording(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override @@ -571,6 +619,7 @@ class CameraPlugin extends CameraPlatform { await _cameraVideoErrorSubscriptions[cameraId]?.cancel(); await _cameraVideoAbortSubscriptions[cameraId]?.cancel(); await _cameraEndedSubscriptions[cameraId]?.cancel(); + await _cameraVideoRecordingErrorSubscriptions[cameraId]?.cancel(); cameras.remove(cameraId); _cameraVideoErrorSubscriptions.remove(cameraId); diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 210fa2baa9d2..f70925b4bede 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -68,6 +68,10 @@ class CameraErrorCode { static const CameraErrorCode notStarted = CameraErrorCode._('cameraNotStarted'); + /// The video recording was not started. + static const CameraErrorCode videoRecordingNotStarted = + CameraErrorCode._('videoRecordingNotStarted'); + /// An unknown camera error. static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index fdfe3e38bb98..f001fe92365b 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/master/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.12.0 <3.0.0" From b8daefb6f36485189dd8a7a3b8f78cd56368380b Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 16 Sep 2021 23:12:13 +0200 Subject: [PATCH 041/259] [google_sign_in_web] Update URL to `google_sign_in` package in README (#4346) --- packages/google_sign_in/google_sign_in_web/CHANGELOG.md | 4 ++++ packages/google_sign_in/google_sign_in_web/README.md | 2 +- packages/google_sign_in/google_sign_in_web/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) 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 7b9eb6b747ec..556f69524026 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0+3 + +* Updated URL to the `google_sign_in` package in README. + ## 0.10.0+2 * Add `implements` to pubspec. 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 501ea14eebe6..4ee1a2956b45 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -1,6 +1,6 @@ # google\_sign\_in\_web -The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google_sign_in) +The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in) ## Usage 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 7075f43151a6..723dbe9ce56f 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/master/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.0+2 +version: 0.10.0+3 environment: sdk: ">=2.12.0 <3.0.0" From 3d32b3667a805329b67931062abad8ef4b9b65a7 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 16 Sep 2021 18:27:05 -0700 Subject: [PATCH 042/259] [ios_platform_images] Bump minimum Flutter version and iOS deployment target (#4353) --- packages/ios_platform_images/CHANGELOG.md | 4 ++++ .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- packages/ios_platform_images/example/pubspec.yaml | 3 ++- .../ios_platform_images/ios/ios_platform_images.podspec | 4 ++-- packages/ios_platform_images/pubspec.yaml | 6 +++--- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index a7270eed0576..2ebd1d1d2d7c 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.0+2 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 0.2.0+1 * Add iOS unit test target. diff --git a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a785..f2872cf474ee 100644 --- a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj index 0ee14af546ef..02e41bc13711 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj @@ -457,7 +457,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -539,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -588,7 +588,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index e45e7694cc2b..97241b677295 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -3,7 +3,8 @@ description: Demonstrates how to use the ios_platform_images plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/ios_platform_images/ios/ios_platform_images.podspec b/packages/ios_platform_images/ios/ios_platform_images.podspec index 485a0e52ffb3..ccbb9f9bda8a 100644 --- a/packages/ios_platform_images/ios/ios_platform_images.podspec +++ b/packages/ios_platform_images/ios/ios_platform_images.podspec @@ -17,9 +17,9 @@ Downloaded by pub (not CocoaPods). s.documentation_url = 'https://pub.dev/packages/ios_platform_images' s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index c3938856e386..adc8dc08011e 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -2,11 +2,11 @@ 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/master/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.0+1 +version: 0.2.0+2 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 701fdb6eab07c99d83fe583f1b7630100fe01a0e Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 16 Sep 2021 19:37:05 -0700 Subject: [PATCH 043/259] [image_picker] Bump minimum Flutter version and iOS deployment target (#4335) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/README.md | 4 +++- .../image_picker/example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- packages/image_picker/image_picker/example/pubspec.yaml | 4 ++-- packages/image_picker/image_picker/ios/image_picker.podspec | 2 +- packages/image_picker/image_picker/pubspec.yaml | 6 +++--- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index de815b1ff302..4c89be1c3e48 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4+2 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 0.8.4+1 * Fix README Example for `ImagePickerCache` to cache multiple files. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index fc4813d1cee5..d8f5835fd402 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -11,8 +11,10 @@ First, add `image_picker` as a [dependency in your pubspec.yaml file](https://fl ### iOS +This plugin requires iOS 9.0 or higher. + Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher. -As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) +As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue. [63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: diff --git a/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist b/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100755 --- a/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index 2f28c9ad2d6d..192962839b24 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -684,7 +684,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -734,7 +734,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 44ae0fc22c06..e11da82d5da8 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the image_picker plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker/ios/image_picker.podspec b/packages/image_picker/image_picker/ios/image_picker.podspec index 0d33b79e61f0..0d6cb0304723 100644 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ b/packages/image_picker/image_picker/ios/image_picker.podspec @@ -17,6 +17,6 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index fca821be2d5f..ba5ce6635ed6 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,11 +3,11 @@ 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/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.4+1 +version: 0.8.4+2 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From d5e7763c0f5c6c31cc56c8053b77aeb7c3cd4859 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 11:02:06 -0700 Subject: [PATCH 044/259] [local_auth] Bump minimum Flutter version and iOS deployment target (#4354) --- packages/local_auth/CHANGELOG.md | 3 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 4 +-- packages/local_auth/example/pubspec.yaml | 4 +-- .../ios/Classes/FLTLocalAuthPlugin.m | 28 ++++++++----------- packages/local_auth/ios/local_auth.podspec | 4 +-- packages/local_auth/pubspec.yaml | 6 ++-- 7 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index c0d04fb5688a..f4129f77f5d4 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.1.8 +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. * Updated Android lint settings. ## 1.1.7 diff --git a/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj index debbb1d06aba..3de4b94f9d5c 100644 --- a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -494,7 +494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -544,7 +544,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index ff7a27203019..3aa8fd848057 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m index a00c7eed2703..c2dc9db25fc8 100644 --- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m @@ -148,23 +148,19 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult) _lastResult = nil; context.localizedFallbackTitle = @""; - if (@available(iOS 9.0, *)) { - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; } else { - // Fallback on earlier versions + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; } } diff --git a/packages/local_auth/ios/local_auth.podspec b/packages/local_auth/ios/local_auth.podspec index b411ddd36067..917c4bf2a0eb 100644 --- a/packages/local_auth/ios/local_auth.podspec +++ b/packages/local_auth/ios/local_auth.podspec @@ -17,7 +17,7 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index 8a31b2f7d501..4f5ef26d9fb1 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/master/packages/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.7 +version: 1.1.8 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 626bc794589b064b7df42d32c95a380a7a59c0ee Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 11:07:06 -0700 Subject: [PATCH 045/259] [path_provider] Bump minimum Flutter version and iOS deployment target (#4355) --- packages/path_provider/path_provider/CHANGELOG.md | 4 ++++ .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- packages/path_provider/path_provider/example/pubspec.yaml | 4 ++-- .../path_provider/path_provider/ios/path_provider.podspec | 4 ++-- packages/path_provider/path_provider/pubspec.yaml | 6 +++--- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index f3f3b2e4da12..764662d5d84b 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.5 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 2.0.4 * Updated Android lint settings. diff --git a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj index 09c902f41869..86528407809b 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj @@ -443,7 +443,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -493,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 61f50ae97262..0001fe580e78 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the path_provider plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider/ios/path_provider.podspec b/packages/path_provider/path_provider/ios/path_provider.podspec index fcadef593d36..86f27c6c8fa5 100644 --- a/packages/path_provider/path_provider/ios/path_provider.podspec +++ b/packages/path_provider/path_provider/ios/path_provider.podspec @@ -17,7 +17,7 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index febfef516f85..5e9bc0b0e7c4 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.4 +version: 2.0.5 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From e1c011f1cef7045ed030812b2a92337c21dfd46c Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 11:12:06 -0700 Subject: [PATCH 046/259] [quick_actions] Bump minimum Flutter version and iOS deployment target (#4356) --- .../quick_actions/quick_actions/CHANGELOG.md | 4 ++ .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 37 +++++++++++++- .../ios/Classes/FLTQuickActionsPlugin.m | 51 ++++++++----------- .../quick_actions/ios/quick_actions.podspec | 4 +- .../quick_actions/quick_actions/pubspec.yaml | 6 +-- 6 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index d893b67b10dc..d2d628cad428 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+7 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 0.6.0+6 * Updated Android lint settings. diff --git a/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj index 745315bcc3d2..d6cb74d0658b 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */; }; 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686BE82F25E58CCF00862533 /* RunnerUITests.m */; }; 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -63,6 +64,7 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -71,7 +73,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -80,6 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -181,6 +186,7 @@ isa = PBXGroup; children = ( CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, + D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -190,6 +196,8 @@ children = ( 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, + 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */, + 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -201,6 +209,7 @@ isa = PBXNativeTarget; buildConfigurationList = 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 3B2E8279C112D7129C8D23F1 /* [CP] Check Pods Manifest.lock */, 33E20B2E26EFCDFC00A4A191 /* Sources */, 33E20B2F26EFCDFC00A4A191 /* Frameworks */, 33E20B3026EFCDFC00A4A191 /* Resources */, @@ -340,6 +349,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 3B2E8279C112D7129C8D23F1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -438,6 +469,7 @@ /* Begin XCBuildConfiguration section */ 33E20B3926EFCDFC00A4A191 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = RunnerTests/Info.plist; @@ -450,6 +482,7 @@ }; 33E20B3A26EFCDFC00A4A191 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = RunnerTests/Info.plist; @@ -553,7 +586,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -603,7 +636,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m index 3a966f86a824..a099b696387c 100644 --- a/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m +++ b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m @@ -24,21 +24,16 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - if ([call.method isEqualToString:@"setShortcutItems"]) { - _setShortcutItems(call.arguments); - result(nil); - } else if ([call.method isEqualToString:@"clearShortcutItems"]) { - [UIApplication sharedApplication].shortcutItems = @[]; - result(nil); - } else if ([call.method isEqualToString:@"getLaunchAction"]) { - result(nil); - } else { - result(FlutterMethodNotImplemented); - } - } else { - NSLog(@"Shortcuts are not supported prior to iOS 9."); + if ([call.method isEqualToString:@"setShortcutItems"]) { + _setShortcutItems(call.arguments); + result(nil); + } else if ([call.method isEqualToString:@"clearShortcutItems"]) { + [UIApplication sharedApplication].shortcutItems = @[]; result(nil); + } else if ([call.method isEqualToString:@"getLaunchAction"]) { + result(nil); + } else { + result(FlutterMethodNotImplemented); } } @@ -57,21 +52,19 @@ - (BOOL)application:(UIApplication *)application - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - if (@available(iOS 9.0, *)) { - UIApplicationShortcutItem *shortcutItem = - launchOptions[UIApplicationLaunchOptionsShortcutItemKey]; - if (shortcutItem) { - // Keep hold of the shortcut type and handle it in the - // `applicationDidBecomeActure:` method once the Dart MethodChannel - // is initialized. - self.shortcutType = shortcutItem.type; - - // Return NO to indicate we handled the quick action to ensure - // the `application:performActionFor:` method is not called (as - // per Apple's documentation: - // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc). - return NO; - } + UIApplicationShortcutItem *shortcutItem = + launchOptions[UIApplicationLaunchOptionsShortcutItemKey]; + if (shortcutItem) { + // Keep hold of the shortcut type and handle it in the + // `applicationDidBecomeActure:` method once the Dart MethodChannel + // is initialized. + self.shortcutType = shortcutItem.type; + + // Return NO to indicate we handled the quick action to ensure + // the `application:performActionFor:` method is not called (as + // per Apple's documentation: + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc). + return NO; } return YES; } diff --git a/packages/quick_actions/quick_actions/ios/quick_actions.podspec b/packages/quick_actions/quick_actions/ios/quick_actions.podspec index b7d56bf3d818..9452fd8c983d 100644 --- a/packages/quick_actions/quick_actions/ios/quick_actions.podspec +++ b/packages/quick_actions/quick_actions/ios/quick_actions.podspec @@ -17,6 +17,6 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index c5d3fe4d4cbe..9531b7027cdf 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+6 +version: 0.6.0+7 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From b312402646853b4a5b0b923a613eb9731733116e Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 11:57:05 -0700 Subject: [PATCH 047/259] [google_maps_flutter] Bump minimum Flutter version and iOS deployment target (#4333) --- .../google_maps_flutter/google_maps_flutter/CHANGELOG.md | 4 ++++ packages/google_maps_flutter/google_maps_flutter/README.md | 2 +- .../google_maps_flutter/example/pubspec.yaml | 4 ++-- .../google_maps_flutter/ios/google_maps_flutter.podspec | 2 +- .../google_maps_flutter/google_maps_flutter/pubspec.yaml | 6 +++--- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 709aa3b90c19..5406dc50e04a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.10 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 2.0.9 * Fix Android `NullPointerException` caused by the `GoogleMapController` being disposed before `GoogleMap` was ready. diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index c80fcb949dad..99c04f3ae1df 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -48,7 +48,7 @@ This means that app will only be available for users that run Android SDK 20 or ### iOS -Specify your API key in the application delegate `ios/Runner/AppDelegate.m`: +This plugin requires iOS 9.0 or higher. 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/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index d15f76352b69..cd614c7e4384 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the google_maps_flutter plugin. publish_to: none environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.22.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: 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 292dda006fa4..35e4f3faf871 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec +++ b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec @@ -19,7 +19,7 @@ Downloaded by pub (not CocoaPods). s.dependency 'Flutter' s.dependency 'GoogleMaps' s.static_framework = true - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' # GoogleMaps does not support arm64 simulators. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 9929eb935df1..641e475a56f0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.9 +version: 2.0.10 environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From f9ec026e24872c70d3fdf57021ba0eb9aa247601 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 15:08:06 -0700 Subject: [PATCH 048/259] [in_app_purchase] Bump minimum Flutter version and iOS deployment target (#4352) --- .../in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 4 ++++ packages/in_app_purchase/in_app_purchase_ios/README.md | 2 +- .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../example/ios/RunnerTests/InAppPurchasePluginTests.m | 6 +----- .../in_app_purchase_ios/example/pubspec.yaml | 4 ++-- .../ios/Classes/FIAObjectTranslator.m | 8 ++------ .../ios/Classes/InAppPurchasePlugin.m | 10 ++++------ .../ios/in_app_purchase_ios.podspec | 4 ++-- .../in_app_purchase/in_app_purchase_ios/pubspec.yaml | 6 +++--- 10 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index e66b5dee6295..04509e56ecde 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.3+4 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 0.1.3+3 * Add `implements` to pubspec. diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md index fcd4834e9cdc..7ac21c495f7b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -24,6 +24,6 @@ If you would like to contribute to the plugin, check out our [contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). -[1]: ../in_app_purchase/in_app_purchase +[1]: ../in_app_purchase [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://pub.dev/packages/in_app_purchase_ios/install diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e44e..8d4492f977ad 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj index 61a5da696986..a88050193053 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -539,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index 79812f609980..b51f622e939b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -169,11 +169,7 @@ - (void)testAddPaymentWithNullSandboxArgument { transactionForUpdateBlock = transaction; [expectation fulfill]; } - if (@available(iOS 8.3, *)) { - if (!transaction.payment.simulatesAskToBuyInSandbox) { - [simulatesAskToBuyInSandboxExpectation fulfill]; - } - } else { + if (!transaction.payment.simulatesAskToBuyInSandbox) { [simulatesAskToBuyInSandboxExpectation fulfill]; } } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml index b916990d3979..0474d70e8b71 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the in_app_purchase_ios plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m index 765ef4dd88d9..0125604b3b3c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -96,9 +96,7 @@ + (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { @"quantity" : @(payment.quantity), @"applicationUsername" : payment.applicationUsername ?: [NSNull null] }]; - if (@available(iOS 8.3, *)) { - [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; - } + [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; return map; } @@ -125,9 +123,7 @@ + (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; payment.quantity = [map[@"quantity"] integerValue]; payment.applicationUsername = map[@"applicationUsername"]; - if (@available(iOS 8.3, *)) { - payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; - } + payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; return payment; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index c0db38e5cfe0..7e2d2ca80675 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -198,12 +198,10 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; payment.quantity = (quantity != nil) ? quantity.integerValue : 1; - if (@available(iOS 8.3, *)) { - NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; - payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] - ? NO - : [simulatesAskToBuyInSandbox boolValue]; - } + NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; + payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] + ? NO + : [simulatesAskToBuyInSandbox boolValue]; if (![self.paymentQueueHandler addPayment:payment]) { result([FlutterError diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec index 785235336e43..3d15b5c0d02c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec @@ -19,6 +19,6 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'armv7 arm64 x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 07eae3ccc702..9ba642e2e590 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+3 +version: 0.1.3+4 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From b073e6e4a0df2cf9029375597e844f3deb95d559 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 15:08:08 -0700 Subject: [PATCH 049/259] [video_player] Bump minimum Flutter version and iOS deployment target (#4360) --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/README.md | 2 +- .../video_player/example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- packages/video_player/video_player/ios/video_player.podspec | 4 ++-- packages/video_player/video_player/pubspec.yaml | 6 +++--- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 69ad92c13fe8..de60af49b95d 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.4 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 2.2.3 * Fixed empty caption text still showing the caption widget. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 4d2bf80a2628..d5e7528fa973 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -12,7 +12,7 @@ First, add `video_player` as a [dependency in your pubspec.yaml file](https://fl ### iOS -Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: +This plugin requires iOS 9.0 or higher. Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: ```xml NSAppTransportSecurity diff --git a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 7cef313627f0..2921ef9161be 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -516,7 +516,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -566,7 +566,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/video_player/video_player/ios/video_player.podspec b/packages/video_player/video_player/ios/video_player.podspec index bd21f4c15365..86230f65db7c 100644 --- a/packages/video_player/video_player/ios/video_player.podspec +++ b/packages/video_player/video_player/ios/video_player.podspec @@ -18,7 +18,7 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 317d46d851b7..926add50f43c 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.3 +version: 2.2.4 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 76d60987a14b0d1811ccf8b69d2ef4a4761ed587 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 15:13:07 -0700 Subject: [PATCH 050/259] [shared_preferences] Bump minimum Flutter version and iOS deployment target (#4357) --- packages/shared_preferences/shared_preferences/CHANGELOG.md | 4 ++++ .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../shared_preferences/example/pubspec.yaml | 4 ++-- .../shared_preferences/ios/shared_preferences.podspec | 4 ++-- packages/shared_preferences/shared_preferences/pubspec.yaml | 6 +++--- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 57b35a81255b..db12fd1829aa 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.8 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 2.0.7 * Add iOS unit test target. diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj index 395e3009aa37..c7567b312596 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj @@ -443,7 +443,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -493,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 8f14d5c1ec5b..1cb0f185baf4 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the shared_preferences plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec index bd239ec5a632..0cb5d35e1dd0 100644 --- a/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec +++ b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec @@ -17,7 +17,7 @@ Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index e3cdfe4f87b3..1e59edf1e12e 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.7 +version: 2.0.8 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 1e92448e8c24fca5685ddfa37acc2535bf481983 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Fri, 17 Sep 2021 15:18:07 -0700 Subject: [PATCH 051/259] [camera_platform_interface] Add web-relevant docs (#4358) --- .../camera/camera_platform_interface/CHANGELOG.md | 4 ++++ .../lib/src/platform_interface/camera_platform.dart | 13 ++++++++----- .../lib/src/types/resolution_preset.dart | 6 +++--- .../camera/camera_platform_interface/pubspec.yaml | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 6567d00aa852..195e142fe10f 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Add web-relevant docs to platform interface code. + ## 2.1.0 * Introduces interface methods for pausing and resuming the camera preview. 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 7a7bbf3da592..aafeef890f1b 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 @@ -64,6 +64,7 @@ abstract class CameraPlatform extends PlatformInterface { /// [imageFormatGroup] is used to specify the image formatting used. /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. /// On iOS this defaults to kCVPixelFormatType_32BGRA. + /// On Web this parameter is currently not supported. Future initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, @@ -71,12 +72,13 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('initializeCamera() is not implemented.'); } - /// The camera has been initialized + /// The camera has been initialized. Stream onCameraInitialized(int cameraId) { throw UnimplementedError('onCameraInitialized() is not implemented.'); } - /// The camera's resolution has changed + /// The camera's resolution has changed. + /// On Web this returns an empty stream. Stream onCameraResolutionChanged(int cameraId) { throw UnimplementedError('onResolutionChanged() is not implemented.'); } @@ -91,7 +93,7 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('onCameraError() is not implemented.'); } - /// The camera finished recording a video + /// The camera finished recording a video. Stream onVideoRecordedEvent(int cameraId) { throw UnimplementedError('onCameraTimeLimitReached() is not implemented.'); } @@ -153,6 +155,7 @@ abstract class CameraPlatform extends PlatformInterface { } /// Sets the flash mode for the selected camera. + /// On Web [FlashMode.auto] corresponds to [FlashMode.always]. Future setFlashMode(int cameraId, FlashMode mode) { throw UnimplementedError('setFlashMode() is not implemented.'); } @@ -227,8 +230,8 @@ abstract class CameraPlatform extends PlatformInterface { /// Set the zoom level for the selected camera. /// - /// The supplied [zoom] value should be between 1.0 and the maximum supported - /// zoom level returned by the `getMaxZoomLevel`. Throws a `CameraException` + /// The supplied [zoom] value should be between the minimum and the maximum supported + /// zoom level returned by `getMinZoomLevel` and `getMaxZoomLevel`. Throws a `CameraException` /// when an illegal zoom level is supplied. Future setZoomLevel(int cameraId, double zoom) { throw UnimplementedError('setZoomLevel() is not implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart index 1724f6c97517..fcb6b83bbf14 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart @@ -6,10 +6,10 @@ /// /// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. enum ResolutionPreset { - /// 352x288 on iOS, 240p (320x240) on Android + /// 352x288 on iOS, 240p (320x240) on Android and Web low, - /// 480p (640x480 on iOS, 720x480 on Android) + /// 480p (640x480 on iOS, 720x480 on Android and Web) medium, /// 720p (1280x720) @@ -18,7 +18,7 @@ enum ResolutionPreset { /// 1080p (1920x1080) veryHigh, - /// 2160p (3840x2160) + /// 2160p (3840x2160 on Android and iOS, 4096x2160 on Web) ultraHigh, /// The highest resolution available. diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index d691afd41c21..41c6a9705482 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/camera/camer 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.0 +version: 2.1.1 environment: sdk: '>=2.12.0 <3.0.0' From c940b709b74a423aa7385d0940e4aa4e76eb7b92 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 15:23:06 -0700 Subject: [PATCH 052/259] [url_launcher] Bump minimum Flutter version and iOS deployment target (#4359) --- .../url_launcher/url_launcher/CHANGELOG.md | 3 ++- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../url_launcher/example/pubspec.yaml | 4 ++-- .../ios/Classes/FLTURLLauncherPlugin.m | 21 ++++--------------- .../url_launcher/ios/url_launcher.podspec | 4 ++-- .../url_launcher/lib/url_launcher.dart | 5 +---- .../url_launcher/url_launcher/pubspec.yaml | 6 +++--- 8 files changed, 17 insertions(+), 32 deletions(-) diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 237f0b139475..4b52a8d46f8b 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 6.0.11 +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. * Updated Android lint settings. ## 6.0.10 diff --git a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj index ffc37abef072..595f85d9a75b 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj @@ -517,7 +517,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -567,7 +567,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index 643c53f9a6cb..db1d548695dc 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m index 6fbc3a522f36..9ba9b1331728 100644 --- a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m @@ -23,10 +23,8 @@ - (instancetype)initWithUrl:url withFlutterResult:result { if (self) { self.url = url; self.flutterResult = result; - if (@available(iOS 9.0, *)) { - self.safari = [[SFSafariViewController alloc] initWithURL:url]; - self.safari.delegate = self; - } + self.safari = [[SFSafariViewController alloc] initWithURL:url]; + self.safari.delegate = self; } return self; } @@ -78,23 +76,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"launch" isEqualToString:call.method]) { NSNumber *useSafariVC = call.arguments[@"useSafariVC"]; if (useSafariVC.boolValue) { - if (@available(iOS 9.0, *)) { - [self launchURLInVC:url result:result]; - } else { - [self launchURL:url call:call result:result]; - } + [self launchURLInVC:url result:result]; } else { [self launchURL:url call:call result:result]; } } else if ([@"closeWebView" isEqualToString:call.method]) { - if (@available(iOS 9.0, *)) { - [self closeWebViewWithResult:result]; - } else { - result([FlutterError - errorWithCode:@"API_NOT_AVAILABLE" - message:@"SafariViewController related api is not availabe for version <= IOS9" - details:nil]); - } + [self closeWebViewWithResult:result]; } else { result(FlutterMethodNotImplemented); } diff --git a/packages/url_launcher/url_launcher/ios/url_launcher.podspec b/packages/url_launcher/url_launcher/ios/url_launcher.podspec index c8bba7e02c3e..6af64b66bee4 100644 --- a/packages/url_launcher/url_launcher/ios/url_launcher.podspec +++ b/packages/url_launcher/url_launcher/ios/url_launcher.podspec @@ -17,7 +17,7 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index 8c46520a71c4..239e3c46c480 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -16,7 +16,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// schemes which cannot be handled, that is when [canLaunch] would complete /// with false. /// -/// [forceSafariVC] is only used in iOS with iOS version >= 9.0. By default (when unset), the launcher +/// By default when [forceSafariVC] is unset, the launcher /// opens web URLs in the Safari View Controller, anything else is opened /// using the default handler on the platform. If set to true, it opens the /// URL in the Safari View Controller. If false, the URL is opened in the @@ -138,9 +138,6 @@ Future canLaunch(String urlString) async { /// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, /// this call will not do anything either, simply because there is no /// WebView/SafariViewController available to be closed. -/// -/// SafariViewController is only available on IOS version >= 9.0, this method does not do anything -/// on IOS version below 9.0 Future closeWebView() async { return await UrlLauncherPlatform.instance.closeWebView(); } diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index cbd1be7bc3fd..c90d2feb08f4 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.10 +version: 6.0.11 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 17517465da20aa3acfe6132202d9e231b43ee157 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 15:28:05 -0700 Subject: [PATCH 053/259] [camera] Remove iOS 9 availability check around ultra high capture sessions (#4362) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/ios/Classes/CameraPlugin.m | 10 ++++------ packages/camera/camera/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index ba2cb313b908..0e5385db44e0 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.3+1 + +* Remove iOS 9 availability check around ultra high capture sessions. + ## 0.9.3 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index cb93e9f5349d..da560d6c4df7 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -532,12 +532,10 @@ - (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset { switch (resolutionPreset) { case max: case ultraHigh: - if (@available(iOS 9.0, *)) { - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { - _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; - _previewSize = CGSizeMake(3840, 2160); - break; - } + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { + _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; + _previewSize = CGSizeMake(3840, 2160); + break; } if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) { _captureSession.sessionPreset = AVCaptureSessionPresetHigh; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 8d578bbdf065..7efc7930cd54 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.3 +version: 0.9.3+1 environment: sdk: ">=2.14.0 <3.0.0" From 1fe19f1d4946c2101a6ebbaed74682b4860009ce Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 17 Sep 2021 16:33:03 -0700 Subject: [PATCH 054/259] [webview_flutter] Bump minimum Flutter version and iOS deployment target (#4361) --- .../webview_flutter/CHANGELOG.md | 4 ++ .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 4 +- .../FLTWKNavigationDelegateTests.m | 19 ++++----- .../webview_flutter/example/pubspec.yaml | 3 +- .../ios/Classes/FLTCookieManager.m | 33 +++++++-------- .../ios/Classes/FlutterWebView.m | 42 ++++++------------- .../ios/webview_flutter.podspec | 2 +- .../webview_flutter/pubspec.yaml | 6 +-- 9 files changed, 48 insertions(+), 67 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 1e1d5aa523ba..99b8a5c419ca 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.14 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 2.0.13 * Send URL of File to download to the NavigationDelegate on Android just like it is already done on iOS. diff --git a/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e44e..8d4492f977ad 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index f75e71d1743a..62428d041adf 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -547,7 +547,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -597,7 +597,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m index 9d3a2aed64eb..08c2e8b60832 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m +++ b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -25,17 +25,14 @@ - (void)setUp { } - (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { - if (@available(iOS 9.0, *)) { - // `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above. - WKWebView *webview = OCMClassMock(WKWebView.class); - [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; - OCMVerify([self.mockMethodChannel - invokeMethod:@"onWebResourceError" - arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { - XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated"); - return true; - }]]); - } + WKWebView *webview = OCMClassMock(WKWebView.class); + [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; + OCMVerify([self.mockMethodChannel invokeMethod:@"onWebResourceError" + arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { + XCTAssertEqualObjects(args[@"errorType"], + @"webContentProcessTerminated"); + return true; + }]]); } @end diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 2316d7941427..6b668eb96af3 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -3,7 +3,8 @@ description: Demonstrates how to use the webview_flutter plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m index f4783ffb4123..eb7c856b250d 100644 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m +++ b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m @@ -25,25 +25,20 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } - (void)clearCookies:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - - void (^deleteAndNotify)(NSArray *) = - ^(NSArray *cookies) { - BOOL hasCookies = cookies.count > 0; - [dataStore removeDataOfTypes:websiteDataTypes - forDataRecords:cookies - completionHandler:^{ - result(@(hasCookies)); - }]; - }; - - [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; - } else { - // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. - NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9."); - } + NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + + void (^deleteAndNotify)(NSArray *) = + ^(NSArray *cookies) { + BOOL hasCookies = cookies.count > 0; + [dataStore removeDataOfTypes:websiteDataTypes + forDataRecords:cookies + completionHandler:^{ + result(@(hasCookies)); + }]; + }; + + [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; } @end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m index c6d926d3cfc2..1604f2756f31 100644 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m @@ -272,19 +272,14 @@ - (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResul } - (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 { @@ -391,25 +386,18 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy case 0: // require_user_action_for_all_media_types if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - } else if (@available(iOS 9.0, *)) { - configuration.requiresUserActionForMediaPlayback = true; } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaPlaybackRequiresUserAction = true; -#pragma clang diagnostic pop + configuration.requiresUserActionForMediaPlayback = true; } 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.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; #pragma clang diagnostic pop + } else { + configuration.requiresUserActionForMediaPlayback = false; } break; default: @@ -468,11 +456,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]; } #pragma mark WKUIDelegate diff --git a/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec b/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec index 1602f1c43daf..2e021994b8f4 100644 --- a/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec +++ b/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec @@ -18,6 +18,6 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 3976ff74fef6..393a66e3f92e 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.13 +version: 2.0.14 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 42b990962578f1d7e7477bea9a4c774ad672b53a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 20 Sep 2021 18:15:46 +0200 Subject: [PATCH 055/259] [camera] Add web support (#4240) * feat: add web to the example app * docs: update README and point users to camera_web for more web-specific info. --- packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/README.md | 7 +- packages/camera/camera/example/lib/main.dart | 86 ++++++++++++------ .../camera/camera/example/web/favicon.png | Bin 0 -> 917 bytes .../camera/example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../camera/example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes packages/camera/camera/example/web/index.html | 39 ++++++++ .../camera/camera/example/web/manifest.json | 23 +++++ .../camera/camera/lib/src/camera_preview.dart | 2 +- packages/camera/camera/pubspec.yaml | 7 +- .../camera/test/camera_preview_test.dart | 2 +- 11 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 packages/camera/camera/example/web/favicon.png create mode 100644 packages/camera/camera/example/web/icons/Icon-192.png create mode 100644 packages/camera/camera/example/web/icons/Icon-512.png create mode 100644 packages/camera/camera/example/web/index.html create mode 100644 packages/camera/camera/example/web/manifest.json diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 0e5385db44e0..62b5f1f9bd4c 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4 + +* Add web support by endorsing `package:camera_web`. + ## 0.9.3+1 * Remove iOS 9 availability check around ultra high capture sessions. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index aa34273fb92c..24566e76bbfc 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -2,7 +2,7 @@ [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) -A Flutter plugin for iOS and Android allowing access to the device cameras. +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) @@ -47,6 +47,11 @@ minSdkVersion 21 It's important to note that the `MediaRecorder` class is not working properly on emulators, as stated in the documentation: https://developer.android.com/reference/android/media/MediaRecorder. Specifically, when recording a video with sound enabled and trying to play it back, the duration won't be correct and you will only see the first frame. +### Web integration + +For web integration details, see the +[`camera_web` package](https://pub.dev/packages/camera_web). + ### Handling Lifecycle states As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so: diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index c0e90eefa3ab..a3a5d1d46391 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -231,7 +232,14 @@ class _CameraExampleHomeState extends State ? Container() : SizedBox( child: (localVideoController == null) - ? Image.file(File(imageFile!.path)) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) : Container( child: Center( child: AspectRatio( @@ -267,17 +275,24 @@ class _CameraExampleHomeState extends State color: Colors.blue, onPressed: controller != null ? onFlashModeButtonPressed : null, ), - IconButton( - icon: Icon(Icons.exposure), - color: Colors.blue, - onPressed: - controller != null ? onExposureModeButtonPressed : null, - ), - IconButton( - icon: Icon(Icons.filter_center_focus), - color: Colors.blue, - onPressed: controller != null ? onFocusModeButtonPressed : null, - ), + // The exposure and focus mode are currently not supported on the web. + ...(!kIsWeb + ? [ + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: controller != null + ? onExposureModeButtonPressed + : null, + ), + IconButton( + icon: Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + controller != null ? onFocusModeButtonPressed : null, + ) + ] + : []), IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, @@ -616,7 +631,7 @@ class _CameraExampleHomeState extends State final CameraController cameraController = CameraController( cameraDescription, - ResolutionPreset.medium, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); @@ -635,12 +650,17 @@ class _CameraExampleHomeState extends State try { await cameraController.initialize(); await Future.wait([ - cameraController - .getMinExposureOffset() - .then((value) => _minAvailableExposureOffset = value), - cameraController - .getMaxExposureOffset() - .then((value) => _maxAvailableExposureOffset = value), + // The exposure mode is currently not supported on the web. + ...(!kIsWeb + ? [ + cameraController + .getMinExposureOffset() + .then((value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((value) => _maxAvailableExposureOffset = value) + ] + : []), cameraController .getMaxZoomLevel() .then((value) => _maxAvailableZoom = value), @@ -708,16 +728,20 @@ class _CameraExampleHomeState extends State } void onCaptureOrientationLockButtonPressed() async { - if (controller != null) { - final CameraController cameraController = controller!; - if (cameraController.value.isCaptureOrientationLocked) { - await cameraController.unlockCaptureOrientation(); - showInSnackBar('Capture orientation unlocked'); - } else { - await cameraController.lockCaptureOrientation(); - showInSnackBar( - 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } } + } on CameraException catch (e) { + _showCameraException(e); } } @@ -916,8 +940,10 @@ class _CameraExampleHomeState extends State return; } - final VideoPlayerController vController = - VideoPlayerController.file(File(videoFile!.path)); + final VideoPlayerController vController = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + videoPlayerListener = () { if (videoController != null && videoController!.value.size != null) { // Refreshing the state to update video player with the correct ratio. diff --git a/packages/camera/camera/example/web/favicon.png b/packages/camera/camera/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/camera/camera/example/web/icons/Icon-192.png b/packages/camera/camera/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 zcmZ`-2T+sGz6~)*FVZ`aW+(v>MIm&M-g^@e2u-B-DoB?qO+b1Tq<5uCCv>ESfRum& zp%X;f!~1{tzL__3=gjVJ=j=J>+nMj%ncXj1Q(b|Ckbw{Y0FWpt%4y%$uD=Z*c-x~o zE;IoE;xa#7Ll5nj-e4CuXB&G*IM~D21rCP$*xLXAK8rIMCSHuSu%bL&S3)8YI~vyp@KBu9Ph7R_pvKQ@xv>NQ`dZp(u{Z8K3yOB zn7-AR+d2JkW)KiGx0hosml;+eCXp6+w%@STjFY*CJ?udJ64&{BCbuebcuH;}(($@@ znNlgBA@ZXB)mcl9nbX#F!f_5Z=W>0kh|UVWnf!At4V*LQP%*gPdCXd6P@J4Td;!Ur z<2ZLmwr(NG`u#gDEMP19UcSzRTL@HsK+PnIXbVBT@oHm53DZr?~V(0{rsalAfwgo zEh=GviaqkF;}F_5-yA!1u3!gxaR&Mj)hLuj5Q-N-@Lra{%<4ONja8pycD90&>yMB` zchhd>0CsH`^|&TstH-8+R`CfoWqmTTF_0?zDOY`E`b)cVi!$4xA@oO;SyOjJyP^_j zx^@Gdf+w|FW@DMdOi8=4+LJl$#@R&&=UM`)G!y%6ZzQLoSL%*KE8IO0~&5XYR9 z&N)?goEiWA(YoRfT{06&D6Yuu@Qt&XVbuW@COb;>SP9~aRc+z`m`80pB2o%`#{xD@ zI3RAlukL5L>px6b?QW1Ac_0>ew%NM!XB2(H+1Y3AJC?C?O`GGs`331Nd4ZvG~bMo{lh~GeL zSL|tT*fF-HXxXYtfu5z+T5Mx9OdP7J4g%@oeC2FaWO1D{=NvL|DNZ}GO?O3`+H*SI z=grGv=7dL{+oY0eJFGO!Qe(e2F?CHW(i!!XkGo2tUvsQ)I9ev`H&=;`N%Z{L zO?vV%rDv$y(@1Yj@xfr7Kzr<~0{^T8wM80xf7IGQF_S-2c0)0D6b0~yD7BsCy+(zL z#N~%&e4iAwi4F$&dI7x6cE|B{f@lY5epaDh=2-(4N05VO~A zQT3hanGy_&p+7Fb^I#ewGsjyCEUmSCaP6JDB*=_()FgQ(-pZ28-{qx~2foO4%pM9e z*_63RT8XjgiaWY|*xydf;8MKLd{HnfZ2kM%iq}fstImB-K6A79B~YoPVa@tYN@T_$ zea+9)<%?=Fl!kd(Y!G(-o}ko28hg2!MR-o5BEa_72uj7Mrc&{lRh3u2%Y=Xk9^-qa zBPWaD=2qcuJ&@Tf6ue&)4_V*45=zWk@Z}Q?f5)*z)-+E|-yC4fs5CE6L_PH3=zI8p z*Z3!it{1e5_^(sF*v=0{`U9C741&lub89gdhKp|Y8CeC{_{wYK-LSbp{h)b~9^j!s z7e?Y{Z3pZv0J)(VL=g>l;<}xk=T*O5YR|hg0eg4u98f2IrA-MY+StQIuK-(*J6TRR z|IM(%uI~?`wsfyO6Tgmsy1b3a)j6M&-jgUjVg+mP*oTKdHg?5E`!r`7AE_#?Fc)&a z08KCq>Gc=ne{PCbRvs6gVW|tKdcE1#7C4e`M|j$C5EYZ~Y=jUtc zj`+?p4ba3uy7><7wIokM79jPza``{Lx0)zGWg;FW1^NKY+GpEi=rHJ+fVRGfXO zPHV52k?jxei_!YYAw1HIz}y8ZMwdZqU%ESwMn7~t zdI5%B;U7RF=jzRz^NuY9nM)&<%M>x>0(e$GpU9th%rHiZsIT>_qp%V~ILlyt^V`=d z!1+DX@ah?RnB$X!0xpTA0}lN@9V-ePx>wQ?-xrJr^qDlw?#O(RsXeAvM%}rg0NT#t z!CsT;-vB=B87ShG`GwO;OEbeL;a}LIu=&@9cb~Rsx(ZPNQ!NT7H{@j0e(DiLea>QD zPmpe90gEKHEZ8oQ@6%E7k-Ptn#z)b9NbD@_GTxEhbS+}Bb74WUaRy{w;E|MgDAvHw zL)ycgM7mB?XVh^OzbC?LKFMotw3r@i&VdUV%^Efdib)3@soX%vWCbnOyt@Y4swW925@bt45y0HY3YI~BnnzZYrinFy;L?2D3BAL`UQ zEj))+f>H7~g8*VuWQ83EtGcx`hun$QvuurSMg3l4IP8Fe`#C|N6mbYJ=n;+}EQm;< z!!N=5j1aAr_uEnnzrEV%_E|JpTb#1p1*}5!Ce!R@d$EtMR~%9# zd;h8=QGT)KMW2IKu_fA_>p_und#-;Q)p%%l0XZOXQicfX8M~7?8}@U^ihu;mizj)t zgV7wk%n-UOb z#!P5q?Ex+*Kx@*p`o$q8FWL*E^$&1*!gpv?Za$YO~{BHeGY*5%4HXUKa_A~~^d z=E*gf6&+LFF^`j4$T~dR)%{I)T?>@Ma?D!gi9I^HqvjPc3-v~=qpX1Mne@*rzT&Xw zQ9DXsSV@PqpEJO-g4A&L{F&;K6W60D!_vs?Vx!?w27XbEuJJP&);)^+VF1nHqHBWu z^>kI$M9yfOY8~|hZ9WB!q-9u&mKhEcRjlf2nm_@s;0D#c|@ED7NZE% zzR;>P5B{o4fzlfsn3CkBK&`OSb-YNrqx@N#4CK!>bQ(V(D#9|l!e9(%sz~PYk@8zt zPN9oK78&-IL_F zhsk1$6p;GqFbtB^ZHHP+cjMvA0(LqlskbdYE_rda>gvQLTiqOQ1~*7lg%z*&p`Ry& zRcG^DbbPj_jOKHTr8uk^15Boj6>hA2S-QY(W-6!FIq8h$<>MI>PYYRenQDBamO#Fv zAH5&ImqKBDn0v5kb|8i0wFhUBJTpT!rB-`zK)^SNnRmLraZcPYK7b{I@+}wXVdW-{Ps17qdRA3JatEd?rPV z4@}(DAMf5EqXCr4-B+~H1P#;t@O}B)tIJ(W6$LrK&0plTmnPpb1TKn3?f?Kk``?D+ zQ!MFqOX7JbsXfQrz`-M@hq7xlfNz;_B{^wbpG8des56x(Q)H)5eLeDwCrVR}hzr~= zM{yXR6IM?kXxauLza#@#u?Y|o;904HCqF<8yT~~c-xyRc0-vxofnxG^(x%>bj5r}N zyFT+xnn-?B`ohA>{+ZZQem=*Xpqz{=j8i2TAC#x-m;;mo{{sLB_z(UoAqD=A#*juZ zCv=J~i*O8;F}A^Wf#+zx;~3B{57xtoxC&j^ie^?**T`WT2OPRtC`xj~+3Kprn=rVM zVJ|h5ux%S{dO}!mq93}P+h36mZ5aZg1-?vhL$ke1d52qIiXSE(llCr5i=QUS?LIjc zV$4q=-)aaR4wsrQv}^shL5u%6;`uiSEs<1nG^?$kl$^6DL z43CjY`M*p}ew}}3rXc7Xck@k41jx}c;NgEIhKZ*jsBRZUP-x2cm;F1<5$jefl|ppO zmZd%%?gMJ^g9=RZ^#8Mf5aWNVhjAS^|DQO+q$)oeob_&ZLFL(zur$)); zU19yRm)z<4&4-M}7!9+^Wl}Uk?`S$#V2%pQ*SIH5KI-mn%i;Z7-)m$mN9CnI$G7?# zo`zVrUwoSL&_dJ92YhX5TKqaRkfPgC4=Q&=K+;_aDs&OU0&{WFH}kKX6uNQC6%oUH z2DZa1s3%Vtk|bglbxep-w)PbFG!J17`<$g8lVhqD2w;Z0zGsh-r zxZ13G$G<48leNqR!DCVt9)@}(zMI5w6Wo=N zpP1*3DI;~h2WDWgcKn*f!+ORD)f$DZFwgKBafEZmeXQMAsq9sxP9A)7zOYnkHT9JU zRA`umgmP9d6=PHmFIgx=0$(sjb>+0CHG)K@cPG{IxaJ&Ueo8)0RWgV9+gO7+Bl1(F z7!BslJ2MP*PWJ;x)QXbR$6jEr5q3 z(3}F@YO_P1NyTdEXRLU6fp?9V2-S=E+YaeLL{Y)W%6`k7$(EW8EZSA*(+;e5@jgD^I zaJQ2|oCM1n!A&-8`;#RDcZyk*+RPkn_r8?Ak@agHiSp*qFNX)&i21HE?yuZ;-C<3C zwJGd1lx5UzViP7sZJ&|LqH*mryb}y|%AOw+v)yc`qM)03qyyrqhX?ub`Cjwx2PrR! z)_z>5*!*$x1=Qa-0uE7jy0z`>|Ni#X+uV|%_81F7)b+nf%iz=`fF4g5UfHS_?PHbr zB;0$bK@=di?f`dS(j{l3-tSCfp~zUuva+=EWxJcRfp(<$@vd(GigM&~vaYZ0c#BTs z3ijkxMl=vw5AS&DcXQ%eeKt!uKvh2l3W?&3=dBHU=Gz?O!40S&&~ei2vg**c$o;i89~6DVns zG>9a*`k5)NI9|?W!@9>rzJ;9EJ=YlJTx1r1BA?H`LWijk(rTax9(OAu;q4_wTj-yj z1%W4GW&K4T=uEGb+E!>W0SD_C0RR91 literal 0 HcmV?d00001 diff --git a/packages/camera/camera/example/web/icons/Icon-512.png b/packages/camera/camera/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/camera/camera/example/web/index.html b/packages/camera/camera/example/web/index.html new file mode 100644 index 000000000000..2a3117d29362 --- /dev/null +++ b/packages/camera/camera/example/web/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + Camera Web Example + + + + + + + + + + \ No newline at end of file diff --git a/packages/camera/camera/example/web/manifest.json b/packages/camera/camera/example/web/manifest.json new file mode 100644 index 000000000000..5fe0e048afe6 --- /dev/null +++ b/packages/camera/camera/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "camera example", + "short_name": "camera", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "An example of the camera on the web.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 6a15896bfa47..5faa69f3cb9d 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -43,7 +43,7 @@ class CameraPreview extends StatelessWidget { } Widget _wrapInRotatedBox({required Widget child}) { - if (defaultTargetPlatform != TargetPlatform.android) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { return child; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7efc7930cd54..b8894d58ac3a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -1,10 +1,10 @@ name: camera description: A Flutter plugin for getting information about and controlling the - camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, + camera on Android, iOS and Web. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.3+1 +version: 0.9.4 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,9 +18,12 @@ flutter: pluginClass: CameraPlugin ios: pluginClass: CameraPlugin + web: + default_package: camera_web dependencies: camera_platform_interface: ^2.1.0 + camera_web: ^0.2.1 flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 14afddaea070..32718f4d5169 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -221,7 +221,7 @@ void main() { debugDefaultTargetPlatformOverride = null; }); - }); + }, skip: kIsWeb); testWidgets('when not on Android there should not be a rotated box', (WidgetTester tester) async { From a8e0129220b52975683433da7353cef472ee8b56 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 20 Sep 2021 13:54:02 -0400 Subject: [PATCH 056/259] [flutter_plugin_tools] Add a federated PR safety check (#4329) Creates a new command to validate that PRs don't change platform interface packages and implementations at the same time, to try to prevent ecosystem-breaking changes. See https://github.com/flutter/flutter/issues/89518 for context. Per the explanation in the issue, this has carve-outs for: - Changes to platform interfaces that aren't published (allowing for past uses cases such as making a substantive change to an implementation, and making minor adjustments to comments in the PI package based on those changes). - Things that look like bulk changes (e.g., a mass change to account for a new lint rule) Fixes https://github.com/flutter/flutter/issues/89518 --- .cirrus.yml | 5 + script/tool/CHANGELOG.md | 3 + .../lib/src/common/git_version_finder.dart | 3 + .../lib/src/common/repository_package.dart | 6 + .../src/federation_safety_check_command.dart | 188 +++++++++++ script/tool/lib/src/main.dart | 2 + .../tool/lib/src/pubspec_check_command.dart | 10 +- .../federation_safety_check_command_test.dart | 314 ++++++++++++++++++ 8 files changed, 523 insertions(+), 8 deletions(-) create mode 100644 script/tool/lib/src/federation_safety_check_command.dart create mode 100644 script/tool/test/federation_safety_check_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 0b3f07da48e1..8dcb4d96d2be 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -74,6 +74,11 @@ task: format_script: ./script/tool_runner.sh format --fail-on-change pubspec_script: ./script/tool_runner.sh pubspec-check license_script: dart $PLUGIN_TOOL license-check + - name: federated_safety + # This check is only meaningful for PRs, as it validates changes + # rather than state. + only_if: $CIRRUS_PR != "" + script: ./script/tool_runner.sh federation-safety-check - name: dart_unit_tests env: matrix: diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7df6913db7d1..3be9173a505b 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,9 @@ - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't have unit tests, rather than skipping them. +- Added a new `federation-safety-check` command to help catch changes to + federated packages that have been done in such a way that they will pass in + CI, but fail once the change is landed and published. ## 0.7.1 diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index a0a7a32b5e0f..1cdd2fcc409b 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -58,6 +58,9 @@ class GitVersionFinder { return null; } final String fileContent = gitShow.stdout as String; + if (fileContent.trim().isEmpty) { + return null; + } final String? versionString = loadYaml(fileContent)['version'] as String?; return versionString == null ? null : Version.parse(versionString); } diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index f6601d39b79e..feece7c1cdff 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -47,6 +47,12 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + /// True if this appears to be a federated plugin package, according to + /// repository conventions. + bool get isFederated => + directory.parent.basename != 'packages' && + directory.basename.startsWith(directory.parent.basename); + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart new file mode 100644 index 000000000000..cb0da162e604 --- /dev/null +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -0,0 +1,188 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/file_utils.dart'; +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to check that PRs don't violate repository best practices that +/// have been established to avoid breakages that building and testing won't +/// catch. +class FederationSafetyCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the safety check command. + FederationSafetyCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); + + // A map of package name (as defined by the directory name of the package) + // to a list of changed Dart files in that package, as Posix paths relative to + // the package root. + // + // This only considers top-level packages, not subpackages such as example/. + final Map> _changedDartFiles = >{}; + + // The set of *_platform_interface packages that will have public code changes + // published. + final Set _modifiedAndPublishedPlatformInterfacePackages = {}; + + // The set of conceptual plugins (not packages) that have changes. + final Set _changedPlugins = {}; + + static const String _platformInterfaceSuffix = '_platform_interface'; + + @override + final String name = 'federation-safety-check'; + + @override + final String description = + 'Checks that the change does not violate repository rules around changes ' + 'to federated plugin packages.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print('Validating changes relative to "$baseSha"\n'); + for (final String path in await gitVersionFinder.getChangedFiles()) { + // Git output always uses Posix paths. + final List allComponents = p.posix.split(path); + final int packageIndex = allComponents.indexOf('packages'); + if (packageIndex == -1) { + continue; + } + final List relativeComponents = + allComponents.sublist(packageIndex + 1); + // The package name is either the directory directly under packages/, or + // the directory under that in the case of a federated plugin. + String packageName = relativeComponents.removeAt(0); + // Count the top-level plugin as changed. + _changedPlugins.add(packageName); + if (relativeComponents[0] == packageName || + relativeComponents[0].startsWith('${packageName}_')) { + packageName = relativeComponents.removeAt(0); + } + + if (relativeComponents.last.endsWith('.dart')) { + _changedDartFiles[packageName] ??= []; + _changedDartFiles[packageName]! + .add(p.posix.joinAll(relativeComponents)); + } + + if (packageName.endsWith(_platformInterfaceSuffix) && + relativeComponents.first == 'pubspec.yaml' && + await _packageWillBePublished(path)) { + _modifiedAndPublishedPlatformInterfacePackages.add(packageName); + } + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + if (!isFlutterPlugin(package)) { + return PackageResult.skip('Not a plugin.'); + } + + if (!package.isFederated) { + return PackageResult.skip('Not a federated plugin.'); + } + + if (package.directory.basename.endsWith('_platform_interface')) { + // As the leaf nodes in the graph, a published package interface change is + // assumed to be correct, and other changes are validated against that. + return PackageResult.skip( + 'Platform interface changes are not validated.'); + } + + // Uses basename to match _changedPackageFiles. + final String basePackageName = package.directory.parent.basename; + final String platformInterfacePackageName = + '$basePackageName$_platformInterfaceSuffix'; + final List changedPlatformInterfaceFiles = + _changedDartFiles[platformInterfacePackageName] ?? []; + + if (!_modifiedAndPublishedPlatformInterfacePackages + .contains(platformInterfacePackageName)) { + print('No published changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + if (!changedPlatformInterfaceFiles + .any((String path) => path.startsWith('lib/'))) { + print('No public code changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + // If the change would be flagged, but it appears to be a mass change + // rather than a plugin-specific change, allow it with a warning. + // + // This is a tradeoff between safety and convenience; forcing mass changes + // to be split apart is not ideal, and the assumption is that reviewers are + // unlikely to accidentally approve a PR that is supposed to be changing a + // single plugin, but touches other plugins (vs accidentally approving a + // PR that changes multiple parts of a single plugin, which is a relatively + // easy mistake to make). + // + // 3 is chosen to minimize the chances of accidentally letting something + // through (vs 2, which could be a single-plugin change with one stray + // change to another file accidentally included), while not setting too + // high a bar for detecting mass changes. This can be tuned if there are + // issues with false positives or false negatives. + const int massChangePluginThreshold = 3; + if (_changedPlugins.length >= massChangePluginThreshold) { + logWarning('Ignoring potentially dangerous change, as this appears ' + 'to be a mass change.'); + return PackageResult.success(); + } + + printError('Dart changes are not allowed to other packages in ' + '$basePackageName in the same PR as changes to public Dart code in ' + '$platformInterfacePackageName, as this can cause accidental breaking ' + 'changes to be missed by automated checks. Please split the changes to ' + 'these two packages into separate PRs.\n\n' + 'If you believe that this is a false positive, please file a bug.'); + return PackageResult.fail( + ['$platformInterfacePackageName changed.']); + } + + Future _packageWillBePublished( + String pubspecRepoRelativePosixPath) async { + final File pubspecFile = childFileWithSubcomponents( + packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + return false; + } + + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final Version? previousVersion = + await gitVersionFinder.getPackageVersion(pubspecRepoRelativePosixPath); + if (previousVersion == null) { + // The plugin is new, so it will be published. + return true; + } + return pubspec.version != previousVersion; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index e70cba24cc5e..70a6ab516037 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -13,6 +13,7 @@ import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; +import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; @@ -49,6 +50,7 @@ void main(List args) { ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(packagesDir)) ..addCommand(DriveExamplesCommand(packagesDir)) + ..addCommand(FederationSafetyCheckCommand(packagesDir)) ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 29f9ea733a03..605a8aa83a30 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -210,17 +210,11 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Returns true if [packageName] appears to be an implementation package // according to repository conventions. bool _isImplementationPackage(RepositoryPackage package) { - // An implementation package should be in a group folder... - final Directory parentDir = package.directory.parent; - if (parentDir.path == packagesDir.path) { + if (!package.isFederated) { return false; } final String packageName = package.directory.basename; - final String parentName = parentDir.basename; - // ... whose name is a prefix of the package name. - if (!packageName.startsWith(parentName)) { - return false; - } + final String parentName = package.directory.parent.basename; // A few known package names are not implementation packages; assume // anything else is. (This is done instead of listing known implementation // suffixes to allow for non-standard suffixes; e.g., to put several diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart new file mode 100644 index 000000000000..4ae3ec5c76d0 --- /dev/null +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -0,0 +1,314 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + processRunner = RecordingProcessRunner(); + final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir); + + runner = CommandRunner('federation_safety_check_command', + 'Test for $FederationSafetyCheckCommand'); + runner.addCommand(command); + }); + + test('skips non-plugin packages', () async { + final Directory package = createFakePackage('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips unfederated plugins', () async { + final Directory package = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a federated plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo_platform_interface...'), + contains('Platform interface changes are not validated.'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('allows changes to multiple non-interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test( + 'fails on changes to interface and non-interface packages in the same plugin', + () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['federation-safety-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('Dart changes are not allowed to other packages in foo in the ' + 'same PR as changes to public Dart code in foo_platform_interface, ' + 'as this can cause accidental breaking changes to be missed by ' + 'automated checks. Please split the changes to these two packages ' + 'into separate PRs.'), + contains('Running for foo_bar...'), + contains('Dart changes are not allowed to other packages in foo'), + contains('The following packages had errors:'), + contains('foo/foo:\n' + ' foo_platform_interface changed.'), + contains('foo_bar:\n' + ' foo_platform_interface changed.'), + ]), + ); + }); + + test('ignores test-only changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('test').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No public code changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No public code changes for foo_platform_interface.'), + ]), + ); + }); + + test('ignores unpublished changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess( + stdout: RepositoryPackage(platformInterface) + .pubspecFile + .readAsStringSync()), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test('allows things that look like mass changes, with warning', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); + final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + otherPlugin1.childFile('bar.dart'), + otherPlugin2.childFile('baz.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Running for foo_bar...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Ran for 2 package(s) (2 with warnings)'), + ]), + ); + }); +} From e314c7a8fdcd2e60934fcf7b44402d6f131f012a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 20 Sep 2021 20:03:06 +0200 Subject: [PATCH 057/259] [in_app_purchase] Ensure the `introductoryPriceMicros` field is populated correctly. (#4364) --- .../in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ .../lib/src/billing_client_wrappers/sku_details_wrapper.dart | 2 +- .../src/billing_client_wrappers/sku_details_wrapper.g.dart | 5 +++-- .../in_app_purchase/in_app_purchase_android/pubspec.yaml | 2 +- .../billing_client_wrappers/sku_details_wrapper_test.dart | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) 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 1a03ba27feb7..7a998d0547de 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,7 @@ +## 0.1.4+7 + +* Ensure that the `SkuDetailsWrapper.introductoryPriceMicros` is populated correctly. + ## 0.1.4+6 * Ensure that purchases correctly indicate whether they are acknowledged or not. The `PurchaseDetails.pendingCompletePurchase` field now correctly indicates if the purchase still needs to be completed. 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 5bbe7504783d..9c349badbb04 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 @@ -68,7 +68,7 @@ class SkuDetailsWrapper { final String introductoryPrice; /// [introductoryPrice] in micro-units 990000 - @JsonKey(defaultValue: '') + @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: '') final String introductoryPriceMicros; /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 49e86087bc13..d21f832a2de6 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -11,7 +11,8 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { description: json['description'] as String? ?? '', freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', introductoryPrice: json['introductoryPrice'] as String? ?? '', - introductoryPriceMicros: json['introductoryPriceMicros'] as String? ?? '', + introductoryPriceMicros: + json['introductoryPriceAmountMicros'] as String? ?? '', introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', price: json['price'] as String? ?? '', @@ -32,7 +33,7 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => 'description': instance.description, 'freeTrialPeriod': instance.freeTrialPeriod, 'introductoryPrice': instance.introductoryPrice, - 'introductoryPriceMicros': instance.introductoryPriceMicros, + 'introductoryPriceAmountMicros': instance.introductoryPriceMicros, 'introductoryPriceCycles': instance.introductoryPriceCycles, 'introductoryPricePeriod': instance.introductoryPricePeriod, 'price': instance.price, 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 d9b09827824b..64a40889f375 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+6 +version: 0.1.4+7 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart index b8ba9d5cc854..62d9104f3738 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -134,7 +134,7 @@ Map buildSkuMap(SkuDetailsWrapper original) { 'description': original.description, 'freeTrialPeriod': original.freeTrialPeriod, 'introductoryPrice': original.introductoryPrice, - 'introductoryPriceMicros': original.introductoryPriceMicros, + 'introductoryPriceAmountMicros': original.introductoryPriceMicros, 'introductoryPriceCycles': original.introductoryPriceCycles, 'introductoryPricePeriod': original.introductoryPricePeriod, 'price': original.price, From 19bc12e2a67d7771410addd7e3dccc1727d0ff07 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 21 Sep 2021 08:40:00 +0200 Subject: [PATCH 058/259] [webview_flutter] Extract Android implementation into a separate package (#4343) * Setup webview_flutter_android package. Creates a new `webview_flutter_android` directory and adds the following meta-data files: - `AUTHORS`: copied from the `webview_flutter` package and added my name; - `CHANGELOG.md`: new file adding description for release 0.0.1; - `LICENSE`: copied from the `webview_flutter` package; - `README.md`: new file adding the standard platform implementation description; - `pubspec.yaml`: new file adding package meta-data for the `webview_flutter_android` package. * Direct copy of "android" folder. A one to one copy of the `webview_flutter/android` folder to `webview_flutter_android/` using the following command: ``` cp -R ./webview_flutter/android ./webview_flutter_android/ ``` * Direct copy of Android specific .dart files. Copied the Android specific .dart files over from the `./webview_flutter` package. Note that the `SurfaceAndroidWebView` class in the `./webview_flutter_android/lib/webview_surface_android.dart` file is copied directly (without modifactions) from the `./webview_flutter/lib/webview_flutter.dart` file. * Modify .dart code to work with platform_interface. Make sure the `AndroidWebView` and `SurfaceAndroidWebView` widgets extend the `WebViewPlatform` class from the `webview_flutter_platform_interface` package correctly by accepting an instance of the `JavascriptChannelRegistry` class. * Direct copy of the `webview_flutter/example` app. This commit makes a direct copy of the `webview_flutter/example` app to the `webview_flutter_android` package. After the copy the `example/ios` folder is removed as it doesn't serve a purpose in the Android specific package. Commands run where: ``` cp -R ./webview_flutter/example ./webview_flutter_android/ rm -rf ./webview_flutter_android/example/ios ``` * Update example to Android specific implementation. This commit updates the example App so it directly implements an Android specific implementation of the webview_flutter_platform_interface. * Update integration tests. Updated the existing integration tests (copied from webview_flutter package) so they work correctly with the implementation of the webview_flutter_android package. * Update webview_flutter_platform_interface dependency Updated the pubspec.yaml to depend on version 1.0.0 of the webview_flutter_platform_interface package instead of using a path reference (which is now possible since the platform interface package has now been published). Co-authored-by: BeMacized * Use different bundle ID for Android example app. Make sure the `webview_flutter` and `webview_flutter_android` example apps use different application identifiers so that the CI doesn't run into problems. * Skip flaky integration_tests (issue 86757). * Exlude platform implementations from build all step. Make sure the webview_flutter_android and webview_flutter_wkwebview packages are excluded from the Build All plugins step as they will cause conflicts with the current implementation which is still part of the webview_flutter package. * Split helper classes from main example widget. Move the `WebView` and related `WebViewController` classes from the main.dart into a separate web_view.dart file. Co-authored-by: BeMacized --- .../webview_flutter_android/AUTHORS | 68 + .../webview_flutter_android/CHANGELOG.md | 4 + .../webview_flutter_android/LICENSE | 26 + .../webview_flutter_android/README.md | 12 + .../android/build.gradle | 57 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 2 + .../webviewflutter/DisplayListenerProxy.java | 147 ++ .../webviewflutter/FlutterCookieManager.java | 56 + .../FlutterDownloadListener.java | 33 + .../webviewflutter/FlutterWebView.java | 498 ++++++ .../webviewflutter/FlutterWebViewClient.java | 323 ++++ .../webviewflutter/FlutterWebViewFactory.java | 33 + .../webviewflutter/InputAwareWebView.java | 233 +++ .../webviewflutter/JavaScriptChannel.java | 58 + ...readedInputConnectionProxyAdapterView.java | 112 ++ .../webviewflutter/WebViewBuilder.java | 155 ++ .../webviewflutter/WebViewFlutterPlugin.java | 73 + .../FlutterDownloadListenerTest.java | 42 + .../FlutterWebViewClientTest.java | 60 + .../webviewflutter/FlutterWebViewTest.java | 66 + .../webviewflutter/WebViewBuilderTest.java | 104 ++ .../plugins/webviewflutter/WebViewTest.java | 49 + .../webview_flutter_android/example/.metadata | 8 + .../webview_flutter_android/example/README.md | 8 + .../example/android/app/build.gradle | 62 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../flutter/plugins/DartIntegrationTest.java | 14 + .../MainActivityTest.java | 19 + .../webviewflutterexample/WebViewTest.java | 23 + .../android/app/src/debug/AndroidManifest.xml | 17 + .../android/app/src/main/AndroidManifest.xml | 42 + .../WebViewTestActivity.java | 20 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + .../example/android/build.gradle | 29 + .../example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../example/android/settings.gradle | 15 + .../example/assets/sample_audio.ogg | Bin 0 -> 36870 bytes .../example/assets/sample_video.mp4 | Bin 0 -> 1053651 bytes .../webview_flutter_test.dart | 1415 +++++++++++++++++ .../example/lib/main.dart | 344 ++++ .../example/lib/navigation_decision.dart | 12 + .../example/lib/navigation_request.dart | 19 + .../example/lib/web_view.dart | 617 +++++++ .../example/pubspec.yaml | 33 + .../example/test_driver/integration_test.dart | 7 + .../lib/webview_android.dart | 62 + .../lib/webview_surface_android.dart | 78 + .../webview_flutter_android/pubspec.yaml | 31 + script/configs/exclude_all_plugins_app.yaml | 6 + 57 files changed, 5128 insertions(+) create mode 100644 packages/webview_flutter/webview_flutter_android/AUTHORS create mode 100644 packages/webview_flutter/webview_flutter_android/CHANGELOG.md create mode 100644 packages/webview_flutter/webview_flutter_android/LICENSE create mode 100644 packages/webview_flutter/webview_flutter_android/README.md create mode 100644 packages/webview_flutter/webview_flutter_android/android/build.gradle create mode 100644 packages/webview_flutter/webview_flutter_android/android/settings.gradle create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/.metadata create mode 100644 packages/webview_flutter/webview_flutter_android/example/README.md create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/build.gradle create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/gradle.properties create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/settings.gradle create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 create mode 100644 packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/example/lib/main.dart create mode 100644 packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart create mode 100644 packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart create mode 100644 packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart create mode 100644 packages/webview_flutter/webview_flutter_android/example/pubspec.yaml create mode 100644 packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/webview_android.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart create mode 100644 packages/webview_flutter/webview_flutter_android/pubspec.yaml diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS new file mode 100644 index 000000000000..4461b602a13b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -0,0 +1,68 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom + diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md new file mode 100644 index 000000000000..d6a10e9b918a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -0,0 +1,4 @@ +## 2.0.13 + +* Extract Android implementation from `webview_flutter`. + diff --git a/packages/webview_flutter/webview_flutter_android/LICENSE b/packages/webview_flutter/webview_flutter_android/LICENSE new file mode 100644 index 000000000000..77130909e474 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/LICENSE @@ -0,0 +1,26 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md new file mode 100644 index 000000000000..38838562d13c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -0,0 +1,12 @@ +# webview\_flutter\_android + +The Android implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin + diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle new file mode 100644 index 000000000000..4a164317c60f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'io.flutter.plugins.webviewflutter' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.webkit:webkit:1.0.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'androidx.test:core:1.3.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/android/settings.gradle new file mode 100644 index 000000000000..5be7a4b4c692 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'webview_flutter' diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a087f2c75c24 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java new file mode 100644 index 000000000000..31e3fe08c057 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static android.hardware.display.DisplayManager.DisplayListener; + +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * Works around an Android WebView bug by filtering some DisplayListener invocations. + * + *

Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged} + * is invoked, the display ID it is provided is of a valid display. However it turns out that when a + * display is removed Android may call onDisplayChanged with the ID of the removed display, in this + * case the Android WebView code tries to fetch and use the display with this ID and crashes with an + * NPE. + * + *

This issue was fixed in the Android WebView code in + * https://chromium-review.googlesource.com/517913 which is available starting WebView version + * 58.0.3029.125 however older webviews in the wild still have this issue. + * + *

Since Flutter removes virtual displays whenever a platform view is resized the webview crash + * is more likely to happen than other apps. And users were reporting this issue see: + * https://github.com/flutter/flutter/issues/30420 + * + *

This class works around the webview bug by unregistering the WebView's DisplayListener, and + * instead registering its own DisplayListener which delegates the callbacks to the WebView's + * listener unless it's a onDisplayChanged for an invalid display. + * + *

I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using + * reflection to fetch all registered listeners before and after initializing a webview. In the + * first initialization of a webview within the process the difference between the lists is the + * webview's display listener. + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +class DisplayListenerProxy { + private static final String TAG = "DisplayListenerProxy"; + + private ArrayList listenersBeforeWebView; + + /** Should be called prior to the webview's initialization. */ + void onPreWebViewInitialization(DisplayManager displayManager) { + listenersBeforeWebView = yoinkDisplayListeners(displayManager); + } + + /** Should be called after the webview's initialization. */ + void onPostWebViewInitialization(final DisplayManager displayManager) { + final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); + // We recorded the list of listeners prior to initializing webview, any new listeners we see + // after initializing the webview are listeners added by the webview. + webViewListeners.removeAll(listenersBeforeWebView); + + if (webViewListeners.isEmpty()) { + // The Android WebView registers a single display listener per process (even if there + // are multiple WebView instances) so this list is expected to be non-empty only the + // first time a webview is initialized. + // Note that in an add2app scenario if the application had instantiated a non Flutter + // WebView prior to instantiating the Flutter WebView we are not able to get a reference + // to the WebView's display listener and can't work around the bug. + // + // This means that webview resizes in add2app Flutter apps with a non Flutter WebView + // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's + // behavior seems to be racy so it doesn't always happen). + return; + } + + for (DisplayListener webViewListener : webViewListeners) { + // Note that while DisplayManager.unregisterDisplayListener throws when given an + // unregistered listener, this isn't an issue as the WebView code never calls + // unregisterDisplayListener. + displayManager.unregisterDisplayListener(webViewListener); + + // We never explicitly unregister this listener as the webview's listener is never + // unregistered (it's released when the process is terminated). + displayManager.registerDisplayListener( + new DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayAdded(displayId); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayRemoved(displayId); + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayManager.getDisplay(displayId) == null) { + return; + } + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayChanged(displayId); + } + } + }, + null); + } + } + + @SuppressWarnings({"unchecked", "PrivateApi"}) + private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // We cannot use reflection on Android P, but it shouldn't matter as it shipped + // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was + // fixed in 61.0.3116.0. + return new ArrayList<>(); + } + try { + Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); + displayManagerGlobalField.setAccessible(true); + Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); + Field displayListenersField = + displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); + displayListenersField.setAccessible(true); + ArrayList delegates = + (ArrayList) displayListenersField.get(displayManagerGlobal); + + Field listenerField = null; + ArrayList listeners = new ArrayList<>(); + for (Object delegate : delegates) { + if (listenerField == null) { + listenerField = delegate.getClass().getField("mListener"); + listenerField.setAccessible(true); + } + DisplayManager.DisplayListener listener = + (DisplayManager.DisplayListener) listenerField.get(delegate); + listeners.add(listener); + } + return listeners; + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.w(TAG, "Could not extract WebView's display listeners. " + e); + return new ArrayList<>(); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java new file mode 100644 index 000000000000..df3f21daadeb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +class FlutterCookieManager implements MethodCallHandler { + private final MethodChannel methodChannel; + + FlutterCookieManager(BinaryMessenger messenger) { + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); + methodChannel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(MethodCall methodCall, Result result) { + switch (methodCall.method) { + case "clearCookies": + clearCookies(result); + break; + default: + result.notImplemented(); + } + } + + void dispose() { + methodChannel.setMethodCallHandler(null); + } + + private static void clearCookies(final Result result) { + CookieManager cookieManager = CookieManager.getInstance(); + final boolean hasCookies = cookieManager.hasCookies(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies( + new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(hasCookies); + } + }); + } else { + cookieManager.removeAllCookie(); + result.success(hasCookies); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java new file mode 100644 index 000000000000..cfad4e315514 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import android.webkit.WebView; + +/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ +public class FlutterDownloadListener implements DownloadListener { + private final FlutterWebViewClient webViewClient; + private WebView webView; + + public FlutterDownloadListener(FlutterWebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ + public void setWebView(WebView webView) { + this.webView = webView; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + webViewClient.notifyDownload(webView, url); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java new file mode 100644 index 000000000000..4651a5f5ae22 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -0,0 +1,498 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.platform.PlatformView; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FlutterWebView implements PlatformView, MethodCallHandler { + + private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; + private final WebView webView; + private final MethodChannel methodChannel; + private final FlutterWebViewClient flutterWebViewClient; + private final Handler platformThreadHandler; + + // Verifies that a url opened by `Window.open` has a secure url. + private class FlutterWebChromeClient extends WebChromeClient { + + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + final String url = request.getUrl().toString(); + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, request)) { + webView.loadUrl(url); + } + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, url)) { + webView.loadUrl(url); + } + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + flutterWebViewClient.onLoadingProgress(progress); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @SuppressWarnings("unchecked") + FlutterWebView( + final Context context, + MethodChannel methodChannel, + Map params, + View containerView) { + + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + this.methodChannel = methodChannel; + this.methodChannel.setMethodCallHandler(this); + + flutterWebViewClient = new FlutterWebViewClient(methodChannel); + + FlutterDownloadListener flutterDownloadListener = + new FlutterDownloadListener(flutterWebViewClient); + webView = + createWebView( + new WebViewBuilder(context, containerView), + params, + new FlutterWebChromeClient(), + flutterDownloadListener); + flutterDownloadListener.setWebView(webView); + + displayListenerProxy.onPostWebViewInitialization(displayManager); + + platformThreadHandler = new Handler(context.getMainLooper()); + + Map settings = (Map) params.get("settings"); + if (settings != null) { + applySettings(settings); + } + + if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { + List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); + if (names != null) { + registerJavaScriptChannelNames(names); + } + } + + Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); + if (autoMediaPlaybackPolicy != null) { + updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); + } + if (params.containsKey("userAgent")) { + String userAgent = (String) params.get("userAgent"); + updateUserAgent(userAgent); + } + if (params.containsKey("initialUrl")) { + String url = (String) params.get("initialUrl"); + webView.loadUrl(url); + } + } + + /** + * Creates a {@link android.webkit.WebView} and configures it according to the supplied + * parameters. + * + *

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

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

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link + * WebView}. + * @param params creation parameters received over the method channel. + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return The new {@link android.webkit.WebView} object. + */ + @VisibleForTesting + static WebView createWebView( + WebViewBuilder webViewBuilder, + Map params, + WebChromeClient webChromeClient, + @Nullable DownloadListener downloadListener) { + boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); + webViewBuilder + .setUsesHybridComposition(usesHybridComposition) + .setDomStorageEnabled(true) // Always enable DOM storage API. + .setJavaScriptCanOpenWindowsAutomatically( + true) // Always allow automatically opening of windows. + .setSupportMultipleWindows(true) // Always support multiple windows. + .setWebChromeClient(webChromeClient) + .setDownloadListener( + downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. + + return webViewBuilder.build(); + } + + @Override + public View getView() { + return webView; + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionUnlocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).unlockInputConnection(); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionLocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).lockInputConnection(); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewAttached(View flutterView) { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(flutterView); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewDetached() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(null); + } + } + + @Override + public void onMethodCall(MethodCall methodCall, Result result) { + switch (methodCall.method) { + case "loadUrl": + loadUrl(methodCall, result); + break; + case "updateSettings": + updateSettings(methodCall, result); + break; + case "canGoBack": + canGoBack(result); + break; + case "canGoForward": + canGoForward(result); + break; + case "goBack": + goBack(result); + break; + case "goForward": + goForward(result); + break; + case "reload": + reload(result); + break; + case "currentUrl": + currentUrl(result); + break; + case "evaluateJavascript": + evaluateJavaScript(methodCall, result); + break; + case "addJavascriptChannels": + addJavaScriptChannels(methodCall, result); + break; + case "removeJavascriptChannels": + removeJavaScriptChannels(methodCall, result); + break; + case "clearCache": + clearCache(result); + break; + case "getTitle": + getTitle(result); + break; + case "scrollTo": + scrollTo(methodCall, result); + break; + case "scrollBy": + scrollBy(methodCall, result); + break; + case "getScrollX": + getScrollX(result); + break; + case "getScrollY": + getScrollY(result); + break; + default: + result.notImplemented(); + } + } + + @SuppressWarnings("unchecked") + private void loadUrl(MethodCall methodCall, Result result) { + Map request = (Map) methodCall.arguments; + String url = (String) request.get("url"); + Map headers = (Map) request.get("headers"); + if (headers == null) { + headers = Collections.emptyMap(); + } + webView.loadUrl(url, headers); + result.success(null); + } + + private void canGoBack(Result result) { + result.success(webView.canGoBack()); + } + + private void canGoForward(Result result) { + result.success(webView.canGoForward()); + } + + private void goBack(Result result) { + if (webView.canGoBack()) { + webView.goBack(); + } + result.success(null); + } + + private void goForward(Result result) { + if (webView.canGoForward()) { + webView.goForward(); + } + result.success(null); + } + + private void reload(Result result) { + webView.reload(); + result.success(null); + } + + private void currentUrl(Result result) { + result.success(webView.getUrl()); + } + + @SuppressWarnings("unchecked") + private void updateSettings(MethodCall methodCall, Result result) { + applySettings((Map) methodCall.arguments); + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void evaluateJavaScript(MethodCall methodCall, final Result result) { + String jsString = (String) methodCall.arguments; + if (jsString == null) { + throw new UnsupportedOperationException("JavaScript string cannot be null"); + } + webView.evaluateJavascript( + jsString, + new android.webkit.ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } + + @SuppressWarnings("unchecked") + private void addJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) methodCall.arguments; + registerJavaScriptChannelNames(channelNames); + result.success(null); + } + + @SuppressWarnings("unchecked") + private void removeJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) methodCall.arguments; + for (String channelName : channelNames) { + webView.removeJavascriptInterface(channelName); + } + result.success(null); + } + + private void clearCache(Result result) { + webView.clearCache(true); + WebStorage.getInstance().deleteAllData(); + result.success(null); + } + + private void getTitle(Result result) { + result.success(webView.getTitle()); + } + + private void scrollTo(MethodCall methodCall, Result result) { + Map request = methodCall.arguments(); + int x = (int) request.get("x"); + int y = (int) request.get("y"); + + webView.scrollTo(x, y); + + result.success(null); + } + + private void scrollBy(MethodCall methodCall, Result result) { + Map request = methodCall.arguments(); + int x = (int) request.get("x"); + int y = (int) request.get("y"); + + webView.scrollBy(x, y); + result.success(null); + } + + private void getScrollX(Result result) { + result.success(webView.getScrollX()); + } + + private void getScrollY(Result result) { + result.success(webView.getScrollY()); + } + + private void applySettings(Map settings) { + for (String key : settings.keySet()) { + switch (key) { + case "jsMode": + Integer mode = (Integer) settings.get(key); + if (mode != null) { + updateJsMode(mode); + } + break; + case "hasNavigationDelegate": + final boolean hasNavigationDelegate = (boolean) settings.get(key); + + final WebViewClient webViewClient = + flutterWebViewClient.createWebViewClient(hasNavigationDelegate); + + webView.setWebViewClient(webViewClient); + break; + case "debuggingEnabled": + final boolean debuggingEnabled = (boolean) settings.get(key); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + break; + case "hasProgressTracking": + flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); + break; + case "gestureNavigationEnabled": + break; + case "userAgent": + updateUserAgent((String) settings.get(key)); + break; + case "allowsInlineMediaPlayback": + // no-op inline media playback is always allowed on Android. + break; + default: + throw new IllegalArgumentException("Unknown WebView setting: " + key); + } + } + } + + private void updateJsMode(int mode) { + switch (mode) { + case 0: // disabled + webView.getSettings().setJavaScriptEnabled(false); + break; + case 1: // unrestricted + webView.getSettings().setJavaScriptEnabled(true); + break; + default: + throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); + } + } + + private void updateAutoMediaPlaybackPolicy(int mode) { + // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all + // other values we require a user gesture. + boolean requireUserGesture = mode != 1; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); + } + } + + private void registerJavaScriptChannelNames(List channelNames) { + for (String channelName : channelNames) { + webView.addJavascriptInterface( + new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); + } + } + + private void updateUserAgent(String userAgent) { + webView.getSettings().setUserAgentString(userAgent); + } + + @Override + public void dispose() { + methodChannel.setMethodCallHandler(null); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).dispose(); + } + webView.destroy(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java new file mode 100644 index 000000000000..260ef8e8b15d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -0,0 +1,323 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewClientCompat; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +// We need to use WebViewClientCompat to get +// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) +// invoked by the webview on older Android devices, without it pages that use iframes will +// be broken when a navigationDelegate is set on Android version earlier than N. +class FlutterWebViewClient { + private static final String TAG = "FlutterWebViewClient"; + private final MethodChannel methodChannel; + private boolean hasNavigationDelegate; + boolean hasProgressTracking; + + FlutterWebViewClient(MethodChannel methodChannel) { + this.methodChannel = methodChannel; + } + + static String errorCodeToString(int errorCode) { + switch (errorCode) { + case WebViewClient.ERROR_AUTHENTICATION: + return "authentication"; + case WebViewClient.ERROR_BAD_URL: + return "badUrl"; + case WebViewClient.ERROR_CONNECT: + return "connect"; + case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: + return "failedSslHandshake"; + case WebViewClient.ERROR_FILE: + return "file"; + case WebViewClient.ERROR_FILE_NOT_FOUND: + return "fileNotFound"; + case WebViewClient.ERROR_HOST_LOOKUP: + return "hostLookup"; + case WebViewClient.ERROR_IO: + return "io"; + case WebViewClient.ERROR_PROXY_AUTHENTICATION: + return "proxyAuthentication"; + case WebViewClient.ERROR_REDIRECT_LOOP: + return "redirectLoop"; + case WebViewClient.ERROR_TIMEOUT: + return "timeout"; + case WebViewClient.ERROR_TOO_MANY_REQUESTS: + return "tooManyRequests"; + case WebViewClient.ERROR_UNKNOWN: + return "unknown"; + case WebViewClient.ERROR_UNSAFE_RESOURCE: + return "unsafeResource"; + case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: + return "unsupportedAuthScheme"; + case WebViewClient.ERROR_UNSUPPORTED_SCHEME: + return "unsupportedScheme"; + } + + final String message = + String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); + throw new IllegalArgumentException(message); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!hasNavigationDelegate) { + return false; + } + notifyOnNavigationRequest( + request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); + // We must make a synchronous decision here whether to allow the navigation or not, + // if the Dart code has set a navigation delegate we want that delegate to decide whether + // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we + // return true here to block the navigation, if the Dart delegate decides to allow the + // navigation the plugin will later make an addition loadUrl call for this url. + // + // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop + // navigations that target the main frame, if the request is not for the main frame + // we just return false to allow the navigation. + // + // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 + return request.isForMainFrame(); + } + + boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!hasNavigationDelegate) { + return false; + } + // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with + // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). + // On these devices we cannot tell whether the navigation is targeted to the main frame or not. + // We proceed assuming that the navigation is targeted to the main frame. If the page had any + // frames they will be loaded in the main frame instead. + Log.w( + TAG, + "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); + notifyOnNavigationRequest(url, null, view, true); + return true; + } + + /** + * Notifies the Flutter code that a download should start when a navigation delegate is set. + * + * @param view the webView the result of the navigation delegate will be send to. + * @param url the download url + * @return A boolean whether or not the request is forwarded to the Flutter code. + */ + boolean notifyDownload(WebView view, String url) { + if (!hasNavigationDelegate) { + return false; + } + + notifyOnNavigationRequest(url, null, view, true); + return true; + } + + private void onPageStarted(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageStarted", args); + } + + private void onPageFinished(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageFinished", args); + } + + void onLoadingProgress(int progress) { + if (hasProgressTracking) { + Map args = new HashMap<>(); + args.put("progress", progress); + methodChannel.invokeMethod("onProgress", args); + } + } + + private void onWebResourceError( + final int errorCode, final String description, final String failingUrl) { + final Map args = new HashMap<>(); + args.put("errorCode", errorCode); + args.put("description", description); + args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); + args.put("failingUrl", failingUrl); + methodChannel.invokeMethod("onWebResourceError", args); + } + + private void notifyOnNavigationRequest( + String url, Map headers, WebView webview, boolean isMainFrame) { + HashMap args = new HashMap<>(); + args.put("url", url); + args.put("isForMainFrame", isMainFrame); + if (isMainFrame) { + methodChannel.invokeMethod( + "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); + } else { + methodChannel.invokeMethod("navigationRequest", args); + } + } + + // This method attempts to avoid using WebViewClientCompat due to bug + // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see + // https://github.com/flutter/flutter/issues/29446. + WebViewClient createWebViewClient(boolean hasNavigationDelegate) { + this.hasNavigationDelegate = hasNavigationDelegate; + + if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return internalCreateWebViewClient(); + } + + return internalCreateWebViewClientCompat(); + } + + private WebViewClient internalCreateWebViewClient() { + return new WebViewClient() { + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + FlutterWebViewClient.this.onPageFinished(view, url); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onReceivedError( + WebView view, WebResourceRequest request, WebResourceError error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } + + private WebViewClientCompat internalCreateWebViewClientCompat() { + return new WebViewClientCompat() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + FlutterWebViewClient.this.onPageFinished(view, url); + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } + + private static class OnNavigationRequestResult implements MethodChannel.Result { + private final String url; + private final Map headers; + private final WebView webView; + + private OnNavigationRequestResult(String url, Map headers, WebView webView) { + this.url = url; + this.headers = headers; + this.webView = webView; + } + + @Override + public void success(Object shouldLoad) { + Boolean typedShouldLoad = (Boolean) shouldLoad; + if (typedShouldLoad) { + loadUrl(); + } + } + + @Override + public void error(String errorCode, String s1, Object o) { + throw new IllegalStateException("navigationRequest calls must succeed"); + } + + @Override + public void notImplemented() { + throw new IllegalStateException( + "navigationRequest must be implemented by the webview method channel"); + } + + private void loadUrl() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.loadUrl(url, headers); + } else { + webView.loadUrl(url); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java new file mode 100644 index 000000000000..8fe58104a0fb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.Map; + +public final class FlutterWebViewFactory extends PlatformViewFactory { + private final BinaryMessenger messenger; + private final View containerView; + + FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + super(StandardMessageCodec.INSTANCE); + this.messenger = messenger; + this.containerView = containerView; + } + + @SuppressWarnings("unchecked") + @Override + public PlatformView create(Context context, int id, Object args) { + Map params = (Map) args; + MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); + return new FlutterWebView(context, methodChannel, params, containerView); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java new file mode 100644 index 000000000000..51b2a3809fff --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -0,0 +1,233 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; +import android.widget.ListPopupWindow; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + *

These hacks are only needed in Android versions below N and exist to create an InputConnection + * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in + * {@link #checkInputConnectionProxy}. + * + *

See also {@link ThreadedInputConnectionProxyAdapterView}. + */ +final class InputAwareWebView extends WebView { + private static final String TAG = "InputAwareWebView"; + private View threadedInputConnectionProxyView; + private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + private View containerView; + + InputAwareWebView(Context context, View containerView) { + super(context); + this.containerView = containerView; + } + + void setContainerView(View containerView) { + this.containerView = containerView; + + if (proxyAdapterView == null) { + return; + } + + Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); + if (containerView != null) { + setInputConnectionTarget(proxyAdapterView); + } + } + + /** + * Set our proxy adapter view to use its cached input connection instead of creating new ones. + * + *

This is used to avoid losing our input connection when the virtual display is resized. + */ + void lockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(true); + } + + /** Sets the proxy adapter view back to its default behavior. */ + void unlockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(false); + } + + /** Restore the original InputConnection, if needed. */ + void dispose() { + resetInputConnection(); + } + + /** + * Creates an InputConnection from the IME thread when needed. + * + *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an + * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the + * system calling this method for WebView's proxy view in order to know when we need to create our + * own. + * + *

This method would normally be called for any View that used the InputMethodManager. We rely + * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the + * system WebView in order to know whether or not the system WebView expects an InputConnection on + * the IME thread. + */ + @Override + public boolean checkInputConnectionProxy(final View view) { + // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. + View previousProxy = threadedInputConnectionProxyView; + threadedInputConnectionProxyView = view; + if (previousProxy == view) { + // This isn't a new ThreadedInputConnectionProxyView. Ignore it. + return super.checkInputConnectionProxy(view); + } + if (containerView == null) { + Log.e( + TAG, + "Can't create a proxy view because there's no container view. Text input may not work."); + return super.checkInputConnectionProxy(view); + } + + // We've never seen this before, so we make the assumption that this is WebView's + // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could + // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. + proxyAdapterView = + new ThreadedInputConnectionProxyAdapterView( + /*containerView=*/ containerView, + /*targetView=*/ view, + /*imeHandler=*/ view.getHandler()); + setInputConnectionTarget(/*targetView=*/ proxyAdapterView); + return super.checkInputConnectionProxy(view); + } + + /** + * Ensure that input creation happens back on {@link #containerView}'s thread once this view no + * longer has focus. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + @Override + public void clearFocus() { + super.clearFocus(); + resetInputConnection(); + } + + /** + * Ensure that input creation happens back on {@link #containerView}. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + private void resetInputConnection() { + if (proxyAdapterView == null) { + // No need to reset the InputConnection to the default thread if we've never changed it. + return; + } + if (containerView == null) { + Log.e(TAG, "Can't reset the input connection to the container view because there is none."); + return; + } + setInputConnectionTarget(/*targetView=*/ containerView); + } + + /** + * This is the crucial trick that gets the InputConnection creation to happen on the correct + * thread pre Android N. + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a + * + *

{@code targetView} should have a {@link View#getHandler} method with the thread that future + * InputConnections should be created on. + */ + private void setInputConnectionTarget(final View targetView) { + if (containerView == null) { + Log.e( + TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + targetView.requestFocus(); + containerView.post( + new Runnable() { + @Override + public void run() { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + // This is a hack to make InputMethodManager believe that the target view now has focus. + // As a result, InputMethodManager will think that targetView is focused, and will call + // getHandler() of the view when creating input connection. + + // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect + // the real window focus. + targetView.onWindowFocusChanged(true); + + // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call + // onCreateInputConnection() on targetView on the same thread as + // targetView.getHandler(). It will also call subsequent InputConnection methods on this + // thread. This is the IME thread in cases where targetView is our proxyAdapterView. + imm.isActive(containerView); + } + }); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + // This works around a crash when old (<67.0.3367.0) Chromium versions are used. + + // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown + // on tablets: + // + // - WebView is calling ListPopupWindow#show + // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. + // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is + // also synchronously performing the following sequence: + // - WebView's focus change listener is loosing focus (as mDropDownList got it) + // - WebView is hiding all popups (as it lost focus) + // - WebView's SelectPopupDropDown#hide is invoked. + // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. + // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). + // + // To workaround this, we drop the problematic focus lost call. + // See more details on: https://github.com/flutter/flutter/issues/54164 + // + // We don't do this after Android P as it shipped with a new enough WebView version, and it's + // better to not do this on all future Android versions in case DropDownListView's code changes. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && isCalledFromListPopupWindowShow() + && !focused) { + return; + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + private boolean isCalledFromListPopupWindowShow() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (StackTraceElement stackTraceElement : stackTraceElements) { + if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) + && stackTraceElement.getMethodName().equals("show")) { + return true; + } + } + return false; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java new file mode 100644 index 000000000000..4d596351b3d0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.Looper; +import android.webkit.JavascriptInterface; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; + +/** + * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets + * up. + * + *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method + * channel to the Dart code. + */ +class JavaScriptChannel { + private final MethodChannel methodChannel; + private final String javaScriptChannelName; + private final Handler platformThreadHandler; + + /** + * @param methodChannel the Flutter WebView method channel to which JS messages are sent + * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method + * channel with each message to let the Dart code know which JavaScript channel the message + * was sent through + */ + JavaScriptChannel( + MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { + this.methodChannel = methodChannel; + this.javaScriptChannelName = javaScriptChannelName; + this.platformThreadHandler = platformThreadHandler; + } + + // Suppressing unused warning as this is invoked from JavaScript. + @SuppressWarnings("unused") + @JavascriptInterface + public void postMessage(final String message) { + Runnable postMessageRunnable = + new Runnable() { + @Override + public void run() { + HashMap arguments = new HashMap<>(); + arguments.put("channel", javaScriptChannelName); + arguments.put("message", message); + methodChannel.invokeMethod("javascriptChannelMessage", arguments); + } + }; + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java new file mode 100644 index 000000000000..1c865c9444e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.IBinder; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * A fake View only exposed to InputMethodManager. + * + *

This follows a similar flow to Chromium's WebView (see + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). + * WebView itself bounces its InputConnection around several different threads. We follow its logic + * here to get the same working connection. + * + *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on + * the IME thread. The way that this is created in {@link + * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to + * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME + * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. + */ +final class ThreadedInputConnectionProxyAdapterView extends View { + final Handler imeHandler; + final IBinder windowToken; + final View containerView; + final View rootView; + final View targetView; + + private boolean triggerDelayed = true; + private boolean isLocked = false; + private InputConnection cachedConnection; + + ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { + super(containerView.getContext()); + this.imeHandler = imeHandler; + this.containerView = containerView; + this.targetView = targetView; + windowToken = containerView.getWindowToken(); + rootView = containerView.getRootView(); + setFocusable(true); + setFocusableInTouchMode(true); + setVisibility(VISIBLE); + } + + /** Returns whether or not this is currently asynchronously acquiring an input connection. */ + boolean isTriggerDelayed() { + return triggerDelayed; + } + + /** Sets whether or not this should use its previously cached input connection. */ + void setLocked(boolean locked) { + isLocked = locked; + } + + /** + * This is expected to be called on the IME thread. See the setup required for this in {@link + * InputAwareWebView#checkInputConnectionProxy(View)}. + * + *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. + */ + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + triggerDelayed = false; + InputConnection inputConnection = + (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); + triggerDelayed = true; + cachedConnection = inputConnection; + return inputConnection; + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return true; + } + + @Override + public boolean hasWindowFocus() { + // None of our views here correctly report they have window focus because of how we're embedding + // the platform view inside of a virtual display. + return true; + } + + @Override + public View getRootView() { + return rootView; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean isFocused() { + return true; + } + + @Override + public IBinder getWindowToken() { + return windowToken; + } + + @Override + public Handler getHandler() { + return imeHandler; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java new file mode 100644 index 000000000000..d3cd1d57cdae --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -0,0 +1,155 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** Builder used to create {@link android.webkit.WebView} objects. */ +public class WebViewBuilder { + + /** Factory used to create a new {@link android.webkit.WebView} instance. */ + static class WebViewFactory { + + /** + * Creates a new {@link android.webkit.WebView} instance. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is + * returned. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set + * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or + * IME, thread (see also {@link InputAwareWebView}) + * @return A new instance of the {@link android.webkit.WebView} object. + */ + static WebView create(Context context, boolean usesHybridComposition, View containerView) { + return usesHybridComposition + ? new WebView(context) + : new InputAwareWebView(context, containerView); + } + } + + private final Context context; + private final View containerView; + + private boolean enableDomStorage; + private boolean javaScriptCanOpenWindowsAutomatically; + private boolean supportMultipleWindows; + private boolean usesHybridComposition; + private WebChromeClient webChromeClient; + private DownloadListener downloadListener; + + /** + * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link + * WebViewFactory} object. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to + * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, + * thread (see also {@link InputAwareWebView}) + */ + WebViewBuilder(@NonNull final Context context, View containerView) { + this.context = context; + this.containerView = containerView; + } + + /** + * Sets whether the DOM storage API is enabled. The default value is {@code false}. + * + * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDomStorageEnabled(boolean flag) { + this.enableDomStorage = flag; + return this; + } + + /** + * Sets whether JavaScript is allowed to open windows automatically. This applies to the + * JavaScript function {@code window.open()}. The default value is {@code false}. + * + * @param flag {@code true} if JavaScript is allowed to open windows automatically. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { + this.javaScriptCanOpenWindowsAutomatically = flag; + return this; + } + + /** + * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link + * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is + * {@code false}. + * + * @param flag {@code true} if multiple windows are supported. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setSupportMultipleWindows(boolean flag) { + this.supportMultipleWindows = flag; + return this; + } + + /** + * Sets whether the hybrid composition should be used. + * + *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the + * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the + * {@link WebView} on Android versions below N. + * + * @param flag {@code true} if uses hybrid composition. The default is {@code false}. + * @return This builder. This value cannot be {@code null} + */ + public WebViewBuilder setUsesHybridComposition(boolean flag) { + this.usesHybridComposition = flag; + return this; + } + + /** + * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling + * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. + * + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { + this.webChromeClient = webChromeClient; + return this; + } + + /** + * Registers the interface to be used when content can not be handled by the rendering engine, and + * should be downloaded instead. This will replace the current handler. + * + * @param downloadListener an implementation of DownloadListener This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { + this.downloadListener = downloadListener; + return this; + } + + /** + * Build the {@link android.webkit.WebView} using the current settings. + * + * @return The {@link android.webkit.WebView} using the current settings. + */ + public WebView build() { + WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); + + WebSettings webSettings = webView.getSettings(); + webSettings.setDomStorageEnabled(enableDomStorage); + webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); + webSettings.setSupportMultipleWindows(supportMultipleWindows); + webView.setWebChromeClient(webChromeClient); + webView.setDownloadListener(downloadListener); + return webView; + } +} diff --git a/packages/webview_flutter/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 new file mode 100644 index 000000000000..268d35a1e04c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; + +/** + * Java platform implementation of the webview_flutter plugin. + * + *

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

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

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

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

Calling this automatically initializes the plugin. However plugins initialized this way + * won't react to changes in activity or context, unlike {@link CameraPlugin}. + */ + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + registrar + .platformViewRegistry() + .registerViewFactory( + "plugins.flutter.io/webview", + new FlutterWebViewFactory(registrar.messenger(), registrar.view())); + new FlutterCookieManager(registrar.messenger()); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + BinaryMessenger messenger = binding.getBinaryMessenger(); + binding + .getPlatformViewRegistry() + .registerViewFactory( + "plugins.flutter.io/webview", + new FlutterWebViewFactory(messenger, /*containerView=*/ null)); + flutterCookieManager = new FlutterCookieManager(messenger); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterCookieManager == null) { + return; + } + + flutterCookieManager.dispose(); + flutterCookieManager = null; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java new file mode 100644 index 000000000000..2c918584ba83 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.webkit.WebView; +import org.junit.Before; +import org.junit.Test; + +public class FlutterDownloadListenerTest { + private FlutterWebViewClient webViewClient; + private WebView webView; + + @Before + public void before() { + webViewClient = mock(FlutterWebViewClient.class); + webView = mock(WebView.class); + } + + @Test + public void onDownloadStart_should_notify_webViewClient() { + String url = "testurl.com"; + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); + } + + @Test + public void onDownloadStart_should_pass_webView() { + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.setWebView(webView); + downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(eq(webView), anyString()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java new file mode 100644 index 000000000000..86346ac08f16 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import android.webkit.WebView; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class FlutterWebViewClientTest { + + MethodChannel mockMethodChannel; + WebView mockWebView; + + @Before + public void before() { + mockMethodChannel = mock(MethodChannel.class); + mockWebView = mock(WebView.class); + } + + @Test + public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(true); + + client.notifyDownload(mockWebView, url); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); + verify(mockMethodChannel) + .invokeMethod( + eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); + HashMap map = (HashMap) argumentCaptor.getValue(); + assertEquals(map.get("url"), url); + assertEquals(map.get("isForMainFrame"), true); + } + + @Test + public void + notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(false); + + client.notifyDownload(mockWebView, url); + verifyNoInteractions(mockMethodChannel); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java new file mode 100644 index 000000000000..56d9db1ee493 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class FlutterWebViewTest { + private WebChromeClient mockWebChromeClient; + private DownloadListener mockDownloadListener; + private WebViewBuilder mockWebViewBuilder; + private WebView mockWebView; + + @Before + public void before() { + mockWebChromeClient = mock(WebChromeClient.class); + mockWebViewBuilder = mock(WebViewBuilder.class); + mockWebView = mock(WebView.class); + mockDownloadListener = mock(DownloadListener.class); + + when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) + .thenReturn(mockWebViewBuilder); + + when(mockWebViewBuilder.build()).thenReturn(mockWebView); + } + + @Test + public void createWebView_should_create_webview_with_default_configuration() { + FlutterWebView.createWebView( + mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); + + verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); + verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); + verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); + verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); + } + + private Map createParameterMap(boolean usesHybridComposition) { + Map params = new HashMap<>(); + params.put("usesHybridComposition", usesHybridComposition); + + return params; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java new file mode 100644 index 000000000000..423cb210c392 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; + +public class WebViewBuilderTest { + private Context mockContext; + private View mockContainerView; + private WebView mockWebView; + private MockedStatic mockedStaticWebViewFactory; + + @Before + public void before() { + mockContext = mock(Context.class); + mockContainerView = mock(View.class); + mockWebView = mock(WebView.class); + mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); + + mockedStaticWebViewFactory + .when( + new Verification() { + @Override + public void apply() { + WebViewFactory.create(mockContext, false, mockContainerView); + } + }) + .thenReturn(mockWebView); + } + + @After + public void after() { + mockedStaticWebViewFactory.close(); + } + + @Test + public void ctor_test() { + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + assertNotNull(builder); + } + + @Test + public void build_should_set_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + DownloadListener mockDownloadListener = mock(DownloadListener.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = + new WebViewBuilder(mockContext, mockContainerView) + .setDomStorageEnabled(true) + .setJavaScriptCanOpenWindowsAutomatically(true) + .setSupportMultipleWindows(true) + .setWebChromeClient(mockWebChromeClient) + .setDownloadListener(mockDownloadListener); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(true); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebSettings).setSupportMultipleWindows(true); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + verify(mockWebView).setDownloadListener(mockDownloadListener); + } + + @Test + public void build_should_use_default_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + verify(mockWebSettings).setSupportMultipleWindows(false); + verify(mockWebView).setWebChromeClient(null); + verify(mockWebView).setDownloadListener(null); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java new file mode 100644 index 000000000000..131a5a3eb53a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; + +import android.webkit.WebViewClient; +import org.junit.Test; + +public class WebViewTest { + @Test + public void errorCodes() { + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), + "authentication"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), + "failedSslHandshake"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), + "proxyAuthentication"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), + "tooManyRequests"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), + "unsafeResource"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), + "unsupportedAuthScheme"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), + "unsupportedScheme"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/.metadata b/packages/webview_flutter/webview_flutter_android/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle new file mode 100644 index 000000000000..1dcd363c9a44 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle @@ -0,0 +1,62 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.webviewflutterandroidexample" + minSdkVersion 19 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..9a4163a4f5ee --- /dev/null +++ b/packages/webview_flutter/webview_flutter_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/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java new file mode 100644 index 000000000000..a32aaebb0ecd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java new file mode 100644 index 000000000000..0b3eeef9b6b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +import org.junit.Test; + +public class WebViewTest { + @Test + public void webViewPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(WebViewTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); + }); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..28792201bc36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b8c8d38d45a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java new file mode 100644 index 000000000000..cb53a7a0dbf5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Extends FlutterActivity to make the FlutterEngine accessible for testing. +public class WebViewTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..00fa4417cfbe --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties new file mode 100644 index 000000000000..a6738207fd15 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..2819f022f1fd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle new file mode 100644 index 000000000000..5a2f14fb18f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_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.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg new file mode 100644 index 0000000000000000000000000000000000000000..27e17104277b38ae70d3936f5afb4ea32477ec22 GIT binary patch literal 36870 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 XfG+?r+!9a#U;+;`01|XOAOHdYynO-T literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a203d0cdf13ea5106cdb8c98b52301f0e3719bac GIT binary patch literal 1053651 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%0A0_A2BB?T)i~ z4N2W5nUMXhn#m5ZjZ%8r@Zo^*hAJ$sW*4*wd7e z-^TSy`GRU+6j(e{#3C)$=TSoCvXhQ~i^5G`ofJ5H1Z3HM)UsT1Coa+z;Y7uEc0L$+P?b`&$h#6;Yn%gIcBF+k7a&xe@o$Bh~szTL@SG zT)$gE=cK;ka@n3^lyUynKCW8B-PT8qelkZD(p2E)OHfV`|IC)anf? zXHgh~dn(MW&9GyMCugeU#Cy||Bqd}VgD9hSYV51>!eh1?d3T5-#)3}6ZJUinQXnP^ z;*${1{1F^$7KNfuI$E)L=5yDpAh4BNO+c5$1xW7x#OG3owTv)i_)h!cjohtgw6V

Jbq-814NPd$TYkZAM*DuEbPgkd!5Mv&pQs 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%0A$ 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

F^h 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�L$2-vNN2qd&Na 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%0D@5 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%0Ar{$@$bdoE3f zl+)Mctbu8+cf2|ZjG4pJdASDk3rIyAtP$Vil5r_2J=$ajGZKuZ(6c+qweFga?LnP& z$B9ce!%KXsjXq5SRfiPXswS-iY((b=Cco(GpJg%HS)DkqdeAqujLe#wR1zIZXdXP{ z{tH_2aQ2**7FUgJ)s3-;>kvl}STp0|mAA_vm-_%l7@!GoQnEZj3wN(80xE$qG2{SZ zIMDDIsc1tgV(E1RH4YJVhAu$OSF2jgifS7B-^nsAh0uNyKAFA00E3gW={GK;j+f~h zohq|x14})MW>4@JUmZX(s#ELWl|&eClZa?zxfeI^w{P$BU}>beL!G4Rep3Q0XhY|# zxX-5XZ-SNl@9g(p$H;TR8^d%KYV;`5n>Y_aoKvj|F~_3CwYLL;Zix|MBKN2Gc{jQ^juMuW*km>*!flJ3%IgUW|mpYG8n(BlD(= zYaIL2*Z$588bvRM9b$(#z4=TEXV9>co`${%P2`k7B7-~6>N(aHVi(&XpuZ-pXx}8J%at2o*u<+f=PXCZey2HqS=mzo{*yq2fLiL}*XZ zFZeeaaI|A3?OjRz)Z6fr<&iM{;EgKuw)kJtQnzdIf=Lz8g5bD!>Ia zdi`-EPy%YiqM4s6@-AJS@SL0+)PksAdKJAS&*Ey};Xakz&^z;~{8{gTcy=q3YYClv zekUcJ%~sr=N{K9hX!$%3c3!|GFk!pb^G}j!x71<{nI&QBJUn2ymOZw%1&_z{D#Qo< z2$?==`s+b@XX*7fe2!fCvwA{W`$X9{i$w*gsz#xQ>Y{}mFMjgCOEcAfXwK!9Sp-=Z z9Usy4Z)(`b$B#j76y_&2ic<4cA7>hWa6R18Pl|eS%{#6@R=(Vqn!n)X_uW$mKnCW> z-*d;?76M%O1oGjbbt%9pej*EB^N1_ObjuI1Zh4YOKs@ncY-7 z2q=(=jyXe)u=tLTJ3$7Qzr*VJkHf9v#IOz>-5mCkOJz9fJ6>I0PLu7>h~29nGp1bc z8WiO5fOo6hUK4?w{!!hGwh^=aI}NAkma6QANzyC+1_`9i7#!|pW&6^cChdHbS5!S- zH!Ul?HZGc4CHp;M^{4EU@Olj=Bd$d*Z}RFF+fef(Zm?`MYCk7|d`lPn!FWqnQ`AA@ z4IU0gubhAd5BZBo`LAS86*b{r+#st5zw7dilQAoUbQ}twfKkI+qFZQi#`)^;o%Ru2 z^`^CM5Ohh6b9B)zbsa008rLxbYPmm=%ERRw~S9bM$Y0wsrahI#{GgU}6$>h<`*GDSby=9`8*n?2Yz8fw^8tTIp z^;e{gUf#2x;_5Dd#V9R@4JRhisFtJp!mXO!JUJxBWdF~6fNCq36aD{VKFHf$goW8; zT%)x&-uAMS6^e8$<9x{7((ihXPIjHt$77YB`Rek-MQUO+U^;GZ_nlOJ@z7=OISFHx z#^;?KyE;~DK58rpLf2Vm6az0E_wR4dR9h@t$)d*z2RLDaG_rYAX1k%auZNovLX;45 zJi)jrGgyF$L<3rvz=5k~dK zvFg1cs&u-v(42YX^B;@Ie*C`pjq+UIHhg;~EMiC1aF)SLYc}a-XDSlqQ7+o;qZhq8YUzO!c{RRD4xm>%0;ACrnaQ=0 zK)ht{r19Q3{cHDA6-qWhSpg�IeT~@r?-xXXpbi!xq0(h5wcR4zyG}?;yV7-{-lT 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�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 z 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

^$ywdz$5hK=ZG`Afn0K9XJjIQXceAoppq9T@ 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 OVI?aqAtfUr_5T3)u@DIW literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..e218d908729c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1415 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_android_example/navigation_decision.dart'; +import 'package:webview_flutter_android_example/navigation_request.dart'; +import 'package:webview_flutter_android_example/web_view.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const bool _skipDueToIssue86757 = true; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl('https://www.google.com/'); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }, skip: _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }, skip: _skipDueToIssue86757); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +

+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }, skip: _skipDueToIssue86757); + }); + + group('SurfaceAndroidWebView', () { + setUpAll(() { + WebView.platform = SurfaceAndroidWebView(); + }); + + tearDownAll(() { + WebView.platform = AndroidWebView(); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + expect(X_SCROLL, scrollPosX); + expect(Y_SCROLL, scrollPosY); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(X_SCROLL * 2, scrollPosX); + expect(Y_SCROLL * 2, scrollPosY); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('inputs are scrolled into view when focused', + (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
+ + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.runAsync(() async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 200, + height: 200, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ), + ); + await Future.delayed(Duration(milliseconds: 20)); + await tester.pump(); + }); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + final String viewportRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(viewportRectJSON); + + // Check that the input is originally outside of the viewport. + + final String initialInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map initialInputClientRectRelativeToViewport = + jsonDecode(initialInputClientRectJSON); + + expect( + initialInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isFalse); + + await controller.evaluateJavascript('inputEl.focus()'); + + // Check that focusing the input brought it into view. + + final String lastInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map lastInputClientRectRelativeToViewport = + jsonDecode(lastInputClientRectJSON); + + expect( + lastInputClientRectRelativeToViewport['top'] >= + viewportRectRelativeToViewport['top'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isTrue); + + expect( + lastInputClientRectRelativeToViewport['left'] >= + viewportRectRelativeToViewport['left'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['right'] <= + viewportRectRelativeToViewport['right'], + isTrue); + }, skip: _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com/"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + expect(error.errorType, isNotNull); + expect( + error.failingUrl?.startsWith('https://www.notawebsite..com'), isTrue); + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }, + // Flaky on Android: https://github.com/flutter/flutter/issues/86757 + skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: 'https://flutter.dev', + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller + .evaluateJavascript('window.open("https://www.google.com/")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion('https://www.google.com/')); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + }, + skip: _skipDueToIssue86757, + ); + + testWidgets( + 'javascript does not run in parent window', + (WidgetTester tester) async { + final String iframe = ''' + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframe)); + + final String openWindowTest = ''' + + + + XSS test + + + + + + '''; + final String openWindowTestBase64 = + base64Encode(const Utf8Encoder().convert(openWindowTest)); + final Completer controllerCompleter = + Completer(); + final Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + initialUrl: + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + onPageFinished: (String url) { + pageLoadCompleter.complete(); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoadCompleter.future; + + expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect( + controller.evaluateJavascript( + 'document.querySelector("p") && document.querySelector("p").textContent'), + completion('null'), + ); + }, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart new file mode 100644 index 000000000000..6176ce255eb9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'web_view.dart'; + +void main() { + // Configure the [WebView] to use the [SurfaceAndroidWebView] + // implementation instead of the default [AndroidWebView]. + WebView.platform = SurfaceAndroidWebView(); + + runApp(MaterialApp(home: _WebViewExample())); +} + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

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

+
+ + +'''; + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + _NavigationControls(_controller.future), + _SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (context) { + return WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +Set _createJavascriptChannels(BuildContext context) { + return { + JavascriptChannel( + name: 'Snackbar', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message.message))); + }), + }; +} + +enum _MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class _SampleMenu extends StatelessWidget { + _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case _MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case _MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case _MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case _MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case _MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case _MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem<_MenuOptions>( + value: _MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Snackbar JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Snackbar.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await WebView.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class _NavigationControls extends StatelessWidget { + const _NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + if (controller == null) return Container(); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart new file mode 100644 index 000000000000..c1ff8dc5a690 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart new file mode 100644 index 000000000000..33773f96cad8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -0,0 +1,617 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// The [WebView] widget wraps around the [AndroidWebView] or +/// [SurfaceAndroidWebView] classes and acts like a facade which makes it easier +/// to inject a [AndroidWebView] or [SurfaceAndroidWebView] control into the +/// widget tree. +/// +/// The [WebView] widget is controlled using the [WebViewController] which is +/// provided through the `onWebViewCreated` callback. +/// +/// In this example project it's main purpose is to facilitate integration +/// testing of the `webview_flutter_android` package. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + static WebViewPlatform _platform = AndroidWebView(); + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [AndroidWebView]. + static WebViewPlatform get platform => _platform; + + /// Sets a custom [WebViewPlatform]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platform` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static set platform(WebViewPlatform platform) { + _platform = platform; + } + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller.updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + userAgent: widget.userAgent, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + } +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._webView); + + final WebView _webView; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_webView.onPageStarted != null) { + _webView.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_webView.onPageFinished != null) { + _webView.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_webView.onProgress != null) { + _webView.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_webView.onWebResourceError != null) { + _webView.onWebResourceError!(error); + } + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + /// Creates a [WebViewController] which can be used to control the provided + /// [WebView] widget. + WebViewController( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + /// Update the widget managed by the [WebViewController]. + Future updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels( + _javascriptChannelRegistry.channels.values.toSet()); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); + } + + Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; + } + + // Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml new file mode 100644 index 000000000000..1e065a6a5b0b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_android_example +description: Demonstrates how to use the webview_flutter_android plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter_android: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart new file mode 100644 index 000000000000..a48e457d55ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an Android webview. +/// +/// This is used as the default implementation for [WebView.platform] on Android. It uses +/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class AndroidWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + assert(webViewPlatformCallbacksHandler != null); + return GestureDetector( + // We prevent text selection by intercepting the long press event. + // This is a temporary stop gap due to issues with text selection on Android: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart new file mode 100644 index 000000000000..6beae105e2e5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_android.dart'; + +/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. +/// +/// To use this, set [WebView.platform] to an instance of this class. +/// +/// This implementation uses hybrid composition to render the [WebView] on +/// Android. It solves multiple issues related to accessibility and interaction +/// with the [WebView] at the cost of some performance on Android versions below +/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more +/// information. +class SurfaceAndroidWebView extends AndroidWebView { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + }) { + assert(webViewPlatformCallbacksHandler != null); + return PlatformViewLink( + viewType: 'plugins.flutter.io/webview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + // WebView content is not affected by the Android view's layout direction, + // we explicitly set it here so that the widget doesn't require an ambient + // directionality. + layoutDirection: TextDirection.rtl, + creationParams: MethodChannelWebViewPlatform.creationParamsToMap( + creationParams, + usesHybridComposition: true, + ), + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated( + MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + ), + ); + }) + ..create(); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml new file mode 100644 index 000000000000..f7db4c6fb63a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -0,0 +1,31 @@ +name: webview_flutter_android +description: A Flutter plugin that provides a WebView widget on Android. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.0.13 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + android: + package: io.flutter.plugins.webviewflutter + pluginClass: WebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + webview_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml index 8dd0fde5ef5f..f6246aae2c86 100644 --- a/script/configs/exclude_all_plugins_app.yaml +++ b/script/configs/exclude_all_plugins_app.yaml @@ -8,3 +8,9 @@ # This is a permament entry, as it should never be a direct app dependency. - plugin_platform_interface +# TODO(mvanbeusekom): Remove the exclusion of the webview_flutter_android and +# webview_flutter_wkwebview packages once the native +# implementation is removed from the webview_flutter +# package (see https://github.com/flutter/flutter/issues/86286). +- webview_flutter_android +- webview_flutter_wkwebview From c05887f7049a9405dfe0156ecc8762ef4d6acca8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 21 Sep 2021 08:40:24 +0200 Subject: [PATCH 059/259] [webview_flutter] Extract WKWebView implementation into a separate package (#4345) * Setup webview_flutter_wkwebview package. Creates a new `webview_flutter_wkwebview` directory and adds the following meta-data files: - `AUTHORS`: copied from the `webview_flutter` package and added my name; - `CHANGELOG.md`: new file adding description for release 0.0.1; - `LICENSE`: copied from the `webview_flutter` package; - `README.md`: new file adding the standard platform implementation description; - `pubspec.yaml`: new file adding package meta-data for the `webview_flutter_wkwebview` package. * Direct copy of "iOS" folder. A one to one copy of the `webview_flutter/ios` folder to `webview_flutter_wkwebview/` using the following command: ``` cp -R ./webview_flutter/ios ./webview_flutter_wkwebview/ ``` * Rename .podspec file to match package name. For the Cocaopod package to be registered correctly the .podspec file name needs to match the name of the Flutter package. * Direct copy of WKWebView specific .dart files. Copied the WKWebView specific .dart files over from the `./webview_flutter` package. * Modify .dart code to work with new platform_interface. Make sure the `CupertinoWebView` widget extends the `WebViewPlatform` class from the `webview_flutter_platform_interface` package correctly by accepting an instance of the `JavascriptChannelRegistry` class. * Direct copy of the `webview_flutter/example` app. This commit makes a direct copy of the `webview_flutter/example` app to the `webview_flutter_wkwebview` package. After the copy the `example/android` folder is removed as it doesn't serve a purpose in the WKWebView specific package. Commands run where: ``` cp -R ./webview_flutter/example ./webview_flutter_wkwebview/ rm -rf ./webview_flutter_wkwebview/example/ios ``` * Update example to WKWebView specific implementation. This commit updates the example App so it directly implements the WKWebView specific implementation of the webview_flutter_platform_interface. * Update integration tests. Updated the existing integration tests (copied from webview_flutter package) so they work correctly with the implementation of the webview_flutter_wkwebview package. Co-authored-by: BeMacized * Fix iOS UI tests. This commit resolves failing UI tests and ensures the `Publishable` task is green. * Point to existing documentation URL Update the documentation URL in the `ios/webview_flutter_wkwebview.podspec` file to point to a valid location. The `https://pub.dev/packages/webview_flutter_wkwebview` package doesn't exists until this PR is published. However the `pod lib lint` step in CI is failing if the URL doesn't exist yet. * Split helper classes from main example widget. Move the `WebView` and related `WebViewController` classes from the main.dart into a separate web_view.dart file. * Updated version numbers as suggested in review. Updated the version of the plugin to the version of webview_flutter package (2.0.13). Also updated the Dart and Flutter versions to respectively 2.14.0 and 2.5.0. Co-authored-by: BeMacized --- .../webview_flutter_wkwebview/AUTHORS | 67 + .../webview_flutter_wkwebview/CHANGELOG.md | 3 + .../webview_flutter_wkwebview/LICENSE | 25 + .../webview_flutter_wkwebview/README.md | 11 + .../example/.metadata | 8 + .../example/README.md | 8 + .../example/assets/sample_audio.ogg | Bin 0 -> 36870 bytes .../example/assets/sample_video.mp4 | Bin 0 -> 1053651 bytes .../webview_flutter_test.dart | 1213 +++++++++++++++++ .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../example/ios/Podfile | 45 + .../ios/Runner.xcodeproj/project.pbxproj | 722 ++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 107 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/Runner/AppDelegate.h | 10 + .../example/ios/Runner/AppDelegate.m | 17 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 11112 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 45 + .../example/ios/Runner/main.m | 13 + .../FLTWKNavigationDelegateTests.m | 41 + .../example/ios/RunnerTests/FLTWebViewTests.m | 91 ++ .../example/ios/RunnerTests/Info.plist | 22 + .../ios/RunnerUITests/FLTWebViewUITests.m | 101 ++ .../example/ios/RunnerUITests/Info.plist | 22 + .../example/lib/main.dart | 344 +++++ .../example/lib/navigation_decision.dart | 12 + .../example/lib/navigation_request.dart | 19 + .../example/lib/web_view.dart | 592 ++++++++ .../example/pubspec.yaml | 33 + .../example/test_driver/integration_test.dart | 7 + .../ios/Assets/.gitkeep | 0 .../ios/Classes/FLTCookieManager.h | 14 + .../ios/Classes/FLTCookieManager.m | 49 + .../ios/Classes/FLTWKNavigationDelegate.h | 21 + .../ios/Classes/FLTWKNavigationDelegate.m | 116 ++ .../ios/Classes/FLTWKProgressionDelegate.h | 19 + .../ios/Classes/FLTWKProgressionDelegate.m | 41 + .../ios/Classes/FLTWebViewFlutterPlugin.h | 8 + .../ios/Classes/FLTWebViewFlutterPlugin.m | 18 + .../ios/Classes/FlutterWebView.h | 32 + .../ios/Classes/FlutterWebView.m | 491 +++++++ .../ios/Classes/JavaScriptChannelHandler.h | 17 + .../ios/Classes/JavaScriptChannelHandler.m | 36 + .../ios/webview_flutter_wkwebview.podspec | 23 + .../lib/src/webview_cupertino.dart | 49 + .../lib/webview_flutter_wkwebview.dart | 5 + .../webview_flutter_wkwebview/pubspec.yaml | 29 + 73 files changed, 4814 insertions(+) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/AUTHORS create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/LICENSE create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/README.md create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/.metadata create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/README.md create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md new file mode 100644 index 000000000000..1a85bc8a53e5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2.0.13 + +* Extract WKWebView implementation from `webview_flutter`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/LICENSE b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md new file mode 100644 index 000000000000..2e3a87b7f310 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -0,0 +1,11 @@ +# webview\_flutter\_wkwebview + +The Apple WKWebView implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg new file mode 100644 index 0000000000000000000000000000000000000000..27e17104277b38ae70d3936f5afb4ea32477ec22 GIT binary patch literal 36870 zcmce7cT`kQv*4X!$T>)6$T>*PAQ>cyz>r5W0+O>JFk}!UNR*reQ9wX4N|YoyCrN_j zpc0j|7k}UP-rKi(_MG?6_UY-V?yjz`uCA_I-M8<9$B*>@DDbZ!KpdTT3l39a+e6?G zA6E}62hZCA2&x4DZomKp{`21td2rkEKf-NG2$(DNnc*b5akxeOk065f4-~@lx9=4WV4z4cdUJlN-GJ^a961@E4y!>MN0%DSaLXtufeEffd z2_A^-Wa;wQ+w!rkjJJyo9hkSbbg^-=^)Pp_1luaQde|~(+1fZ*GRS+|IJnY-2(~WP zt~RzFGGN!%VAnz-w_P(hIavL}6%mNyZ0YFgVQ%FCl9JI9mS-TmEpu=I%Y1A-JVDN6 z_~BrYwX3rm$f%W*?LWAlf03QtgasWuU7bP601o=zE_zxz3Nj4;a13_z7Z4Zz7a%D9 zFW{eg5k-lA*MJ58tP4$>W$0?&LeJH=vOPy7HnR2N)UiZTP4xN&B2r8>IK8wLFw4S+bv3HJ z64fzTy>uj>NTibJapqVXC^$B~rq%mWQIg>q7c!cNxaG55j)=Mj2hafkqR0Y{=g{!T zaws4O0I(H~_-`C9nIv9XOOXFX0aX>8uXZ=G`$=R2W3*c1JVWEsdTF-C|BA>NZzO}T z&7E7SavYdc-IPS(DK#Wm_{Qh}12{jS99=|c{}vC{Lpb9`LD5pcA+ZMt(S?B!W9=h} zoq}Qz`g8^lIBYrtp(M*kjGG0?AVh3MOV$0#1B4ihvjw(tZd-gh%k?~vm;s@jq9yt5 zvPlq1O@1V>om2EE;E;6+uCVj^A9{b0|0cP&%|NJPbII`^=x_GFCI5&A9Iqv%XtaB8 z@!5QUWd?hs!)EbWZov09kMaC*szK=R({1OsZT}fLxT+Y#iFDN%BgnuclG3V=F^V=6 zi>s`-@HyApH@68-QJmlxj$|<5DT~iIV|z-VKRUn%DI!Ny3_V2vGmvQ4E^F6Ara9N64CP%G4xb6^wyg3Ld@KzT86g( z($M=hy^X@R>BN8Ok>c+V)c{~iJbvxpkp)t~_znQ5ZncTy`M2BRmn`?wUK?kAH!fsz zEPV64avS;=MeY{mHgKdBa%L7gWR@6bmVoL1AX5KT@I(O6ai{GCV-h$jTGfA~3$6ty zpo0gFRP`lK|4ZS48ey#?rH5WxQ^Lpz*ngb>u=(vAXiW%%=|6z##BKKf8F>b3ZX+mI zR&dn;?IzB3)AL;48H(qr3&@J7xj)kuQ%kLdpaz9T3@Cvk z?_uaA^6ZmfFH0tq*nH!qPmM+QI$rm2S#74(AV+G3ZuyI_cwJTAq7+9SPM=BLxYzZd zwm@mYA5S>QJ`8vW0O0~^oW*2TaT2AJN^zXMlwOen7<8WT@|)S=x?GzXUOMWZDLvyQ z-eiaA3bLn$>8P{k000WW1b?^%qltGQa@hb%{-yMTU&0SlgddP8>vLFyt^bvXp+c|xNVZHQ-u#BEQVni4*rH_~sK_GmT5mUAk2yd`<(Z_ji25C$l^bldY zsE;%iL(EJgji&<*(*n(ZNh3})C%k1QypU7g&ZgeTg;RY!AFU~G+X-*mId5BIL!@zo z2v|FYu$}Qj&iU9HrX%L+yeGT^L1c47Tf=i(gjl%G=P#8_X_) z$TM0qC(fn~FU-@N7sZe>rwHTIK!lI8`Ly?1hWA{94B{kEFD+u(3u)dExJYHUKpr{f z9cZ5FY;I&bcjk?hm~VEWE!xFt<7(-A-(17D*Q(v*Z7T6-VPl}ag7(7!Zd)a;>cUxS zrSnBk`9P<@ZoS)Hk27uC4^mmg)a?M5Q*4*hy-g`ykd*NtWn`-DGAIp77gLHz(=)1~ z-9MdN1Fco_H3DB-dGT8KX?ve6JTsn2tt_|3Z5y9?QS@X%VXi)+#zn|y`SD(lHYI3n zR=@{>g#s0au#GZ6#12>_u{=!S86aaFAOW=*i4fMBNfjOv78w#kXdP)z2_q1qT8LC$ zEg`L;34OgI;R#{HLj$d;6vTwEGdQuPU~N6paN2t&)pkKx%TN@dmj>e78iEs^YHONy zOKL<467KE`VI4UrU7$Vi0Ayq~ov<&MIQ>nHCN16yyk(py9JG2yL+@CHfgPmwAcP6>~B zA;9dE7Xo<%>X*_(gvgku7DyR1U`Pb_?Vvsg2F+9Jg3>3QrIo9E;Lt=YW*Xo+qIL5TmVT^s7$2TP9eN! zgb{P;&c+S4bJKyh{2`EYqR6RwTjP3f)!LoQ5%+yo*F z0vAsM3wYnPasz-xI|N{84;R$2?^6dDaKLGg6xd`C#sR&Ga23wY?Btgc7>wcif}0HC zI=Y**;kvpz+2G<=FG$hXb>vOa)m5(mANM+cbtSCi^EV(OvK$8JLIAvYYvbISL7J}CLZF42hZY#(0lCrlo z+yk9%Z$acro(0J@eels?Yg+HT3a;8T=Y@K2^K-e-r)xJ^P;(|95DIJB&y&EKzq@9+NQ~gRvyrlR$Ed%z6;yA)JT{^o|HXm(6Ba0+TMBM4=z71+yvR ze?2yJ?AKv`9XcI#tb%YIa1)Snn@s`rjS)oVEJ)E+z+w%*RX;KSF#5Zmx^}SGm90bm ziU0-buWdnOA`dFjI?>FUGE85oIK`U4sW&^>K!RdkE>*6>QfZFflhMo$`G;kR7Ny+mLIRzyZH4QBt{IBDc4+Frx zUN0sQ@t9deH@Zq>4~A+KxDNjnh=@Qp8VsOE2@nzevvs}Q(%y#Kt?RAt`Y1mtg^i1& zq2|Mf4~;cVpK9CN>)F^@+1PpRa&vI7vvG4Y)-}{MeQYZ)#9MqLWeCB_r=NwOrq5ba z^*Ha~Zt|U>kx2J+mQ#=_303wRycc@Z?OI~J z{ctVh<3*3x@>qK9ES%UgZb!#dIOQkFYKW??*Vv zXdJH}k7DGzjypqLR@&nl*X`egIq3|*NAL%#7*O>M5bMJEp1aoN00!`+A1 z`6pTT!1PmB;YJS|vG>*y zPBn#y?-2u;=@xd}pEGCZl9YDMIBD-W^7ypMhVpFe4N8&eP2f3cIqb?;>!m7`T}OoP z1BjI2h;Ymdz=#L9{)7A_ifPM^QUHW3N8;`*dTNoZvH%Z%C@WcAGk-qZ<3984JEbq# zbIbK;#-1|kKi?IO8Nq6i&3MZkyt~sPk%z}Sga5D}bKtXYA?Bh@MwiPt8vF6aFBL5Y z0c>WC;&mkst-0x!!^g*oX;3KNKCE}iZ8_o6B`U<1lujSG~ZR{=|Zawev`=YY{I3xip$FkVca zy7q^$YH>GM5AMGD7{E|&xR#!7VA!bB)-Ap9?5=B4sW87geFIfXTZYJ$KQxCTuFUXj z1L0CuLz~c{7(XVTKu|-wudKui>$sz!-|4vWxbmLDR7#$M^WPX3s1GI%-dx#?{rpLE z5D*&hw&TL;oaU*(ez4%pCm{OXN>5zR3q?qk#?xL-@7ZZS%_yRh&rF{ffMXaLp(j*1 z3TA{WjKU7OkeL_NU|~mL@Z)-Xz}L2qB_|e$UO89&Aub|Ddeq?R!FEo0R<?KhB_`4sCa%^uUb#vXa6lo=n=Hd9Tb*Gp*2>=5MsURcfVGbQ*^F=6CHft`rC$ zyn`7e@%at!O&&Gt)Kqs`0>R(cI}3$P^NrH0 zPg#y+7du}&^v|J`?afA^V?2-Kl)Hgm2!5|mynvBlprE;3{5zT+&AJx zSRz2ELn3DdmA(9A{pcRKF&aOm1^N1%?PztPE+7{^7Do=h!8((64%)Z>wcWM!WWQob zzsa!Zr~GI@29;3iGvwO%L6Kumh{7WT6}S z$U#9&>xd%Pc}wrRQ>D9PXEobxg|D{S->~|86e{G;{6IOTBhR2-?<{Y~FT}I2t48m* zVG=2OlzVQ##A^l1!U^Ss`;9W1V_Il^K+9(i!9>B9EPr{3oF z2vv_N&cwvn=$seZ&(@Ct5r_yY82LiJQLQ50%vXWk@Up~HrKZop=q5!9% zu+lyk-?)LUCUd8Fq91$xuG~Mv{PYmm7xi@wJQ6j!V&O2q+I&_OEt!G4Bx0cj&aPgL z5HzHF%Eq;0tQIh!_gVYyey#QFgYIJ5vV6i_>6)84@L%6 zK=J+*OOt(~94XEaQwG%yR_gNZmPM|;0nNHfVDFD+fomo9gnOI6y^O^L*6VZ;SuI+0 zJ%$CCkrlp^&!S4G<^9oTT!%dab|2_wihhX6awMX-)A!&T!IT51yLFq1G~@czMtyzr z=j``HuhS>4uDyDi7~3qvit0;ghc4BEQl=E!EvOFu5U9Okv?=LBg)7fRg_o!={Llg5 z0P+ZhG0O8JA4mj1Gm?5G{FohsT3Cqm)IN@kDkFUu*3M9irA&!?gLUY+7If&je{wkK zr4v>SPk&kz+8d>bshu8Rkk{xG;D3`{D8I?04b5~a@!Pp4b}_-Yf3a`P+Hh~#aB3A5 z3KzvagVz&_%=`jQyf^NtPBD?cBd_I`x$4aAT+`BPv9%V{%I4q68+us#9i|elMBJc4 zNfZBkebe_1m$V07*X-DEm{Yv#_xo0KqZw&EP^WHd9=XgF2m?e5;z}WhX#s^_AlF(I zSh9djVl+=N59Z92Ma~JM7%bkn?+H4pXTE10v?Ql*XMA_xrJDqqt?%y>pW0sGc&CiV z$M_pWNcA(SiRIujkH80;XvGWcoQAyHTn43tTuu&izvf_X>D+y3q(|%EZvG=$tHaxv zHGM~`#5Fi=lMF@HEOy}KnFc4uV#_NxX2umqr zIkmZDDAOx6S^WIO(l{cB6B@Y)$UR}ug31-iM_~Y>w5`_5hC1u8s#NTZ*)@fQwaMPjx>>dN}(@JpMgSzX!$s+G&u0b(z{nEv5-U&)UAAH`r2! zWyS+bKULv{(0!gh2}( zT2>-G02{4_0kYi2P1|PhIBE%EybV~xBIf(`#P&0ih(Q-^fOc7K7Bq*WNC3$C;*l?X zFTE~L597!$NPOAid9v}@$V^kwo8)ZuQ1S?D7UN3mOW%?5Ees;`eBOAMn}8BG^Q8PU z+dFf`N|`OE4wE;g$jq>x{0U!2?-l?oi>Q~(ax5s&XC`2CO^MrC5|M~gJ8GEDeW5lzZFUSr+w;Z5=;U&+EJ3f-(oziyK+>>1mTAIh2J|Ar1-dL#fd1g;yt!u6FH&0R~j$C;A8Gb1eZJvA-+e1D0(Q zr;Me{nkDWp&;XwANiQ+60Ivr?dt|GB31vdW*M>OdPUm4NRZ`5?d&p98HoHiQ6<={i z0B>Yzmm7j4OX=^*|(VJjrbD(?D!> z(|6*h1wlsU!_0(}Im46(oW)S_ZewP{-_PyTQ!9R#MVa1vlk`UFQve<{Q~H6u($~;3 z`e62hM$d>7k;bixH>0VN2ceuhZx$mzzy%{tKp zLf#9rk7|yfF!b&a=A)20fDtJ4oPlXQJDPMd$qcHe!i>)Rqy&Ic3YJN@z%}vo9@_6z zN%ot4ylJB>ZnNWWMD>*h@lI28A3xgjYz4#*V0-A)34|=W=&G-Gj(8aEX(|e7)p;_%$RcJ*)dPhy3lcn!M??=>zzsV5o zSnqE+9aVk7L0lKh@TJcPM-szD^0oXcHuVB;R6mD3hfx~3Gs3*n6TQ9BRW6G7hJ;sSFy` zmvtGBT?2*Ap7YN`=?S)OnOJYMY9BHnJ19 zoMj%HoPNx)RB9gUUN-a%UrpMcRk6m?<*g&Bw-bN5u~=BYqIIOH*R_P&thWHEn;CNy z7xLy~_NHToR0?TUWFcpzfyt?aH*Kln_5}H4>HfgvE{rN?gI=ES-RGVY8gc( zZI}~+F(yWaZPq!*x*{Bgr+{k$J9;%uRI@nk?Tr0zD+WI zLcybCb;xNet=G4)=@$Nn)FQBE7pahOh0tI(>qsLn*{h6h&;C4R^t1j@UyM6Cn{(J4 z7S{1>tG$T89t9)G$VilAfcP=!0FP*7|2&FfM#I$DM#V@&eQbBWwujbJDpgS`!HyEz zM=5IEZ@QcwxU~&5%=YZHG8_gvCZnr<_7)>s5O=(@QIiuOV7K{}&2Z5MOY%Gh*-J9m z=>Nq3;oQ89iE!K?T#eg4f zJK7_>R%?^@26I6W0|ji0B?hq?3M&C8v=AoQRCa4VUL-sckZ)PyyZFr~H zp2;29bzkcJ&v*kF##XHbvdL$2mNr;ZG9jy*5A5P8LX5;8z8q$aC-lmM9CeA*ZSiM) zq4{jK&~`FTWtySny079%p>EZH2q75a&n}QSBD2>7Y(mkE*w3|Y zLWMj%7H~_joLW$hfH1kG*>#c$S_0v6;(ns)J}Qb70Dg(32VDodCgfxil8|LG-}{i+ z>gg$q*yxUyB|VKJTepu7@E?obl6dT>qyJWqyUsX;h+_VGm*KqK^M1%@cg-i-%OPm4 zT}suCFCago6+V=Cm9eE3^`viwID4!8SqEO9q4^c~#4bjjwVC)tNoUmWV}$ zm9c_l)DA-t{Oh!8ifEWuP;-2 zUB5k>{KG=(c6dm5_5)sdov2=8|EpK1Sm@)g-hOp~IIZ9P8<0(D^W3{9r4dwoGojTn z!yz>KRG|7TGU6P=`_oFneUDb81#+Bq$?T1U zNPftr16acO03E<4E8VJf;cZh(ial(~kRkBY1`|91@?_*$fsz6k!X?qt*I3R`7iTzP zv^Q=`4X<&|kN2q`0@geLuwr&|)-(20Ot0E@XSkNTj(#4Vjc{j{@t>iN-7#~u#y(JT zH1_gZaN&2XHQ1edcHk|u@t8MJWO$t1%z(Yr0@qu}nbG{%<9&YmTvpnfOy`S0&kn== ziOlEyVG^+t6fm8!!;Xh;N(9U_lp)7d7g4(7LIVoZ}<{1Xn1H zmf}^-*bZ3D!0y{Cx!$sGmJ^dOX-Q&?jV()ETFH{#|;6UgshE5}Akl+7WmO#n^N#&tnc^7OPnR5_^=z z-|fBea))wR8NZr^-QG6q=Q)+nww91u0cE8OfotY1(%(my4PRTwg_I2+us&IZu5UhS z=+rBfqxgy?>{qq=?#RF9lq`L(dpWrKj$p~z>Z5+wGaI=XL}A$4p;A9%>NP*1@A9o?u5!sek^N>x~0!o*}n)Dax+ zZsck%mL4f2?XyhYdT-ws_XBPgamJ0rW9As%p!>Nhk?t&at-8kaKH;}{yqR=TokkCv zdnCcl_$-qbElWag@~}k?A#7F5f7N|i=!rPHuXO*{6nWC(-4C&I58iHW#cbH)z4&Qs zuGx4{!WT6zAHhUPQIAKCrXsfD!kS_9xNG<_#oZ?8&kW9;POk*)S`>D>f-DYTgkl;6 z3S|Iv0509C`S?#?+YvaFdPqI@Sp^p;w=FD0pC$aYoWI(m~aeGZGM|dd8o}s)g1xV3!yp z`TV;m#&OWWhj7i)DyvCz+v1&9+ppVgPrgy9q`coQ)MyAZ4W*7-x4VFB^kXUCjj5I) zxtw`0Sy&xYGhbY%Z8T2Q%O=gxuE6F+2O9-&Mga>L94}~aH^3%bu|2-4_iINyuYG}X z_zm&Y@@`u)>-S{D>e)0a zF^O;#9<97&xL}?YnG}aEB?itP@gI11DpOQOs4vs1_&eSDwU5MBig#*GRq8}VN=AJfA3B+_1W z%zVHqX_;7Xd@uRq7{lqZXfaZtTqeN(ll5Z8AUYx}`-eD2HtxV+3*4rr;n&!FqOkW98PY@LF=>5|V2aC8k(b z0iNVZs&{MK*&i<-lh6joh`yrZCt!d`147vCVNgv;;8wFwUcD8EFv0lIn&v*&v%Z$+ z5FYw@#2nrCO-eiYQp-cb)ia1gIyU}DfL?K4JfM#Nt@HcprSQUDap`c5S%Cf5vmm?o z4Y`2-Zj)FdE$`%qLcB?K=#zm)~ovGI`sR=sGLhrGHvt-)+Bj*R)O=yg^a5 z8~rzTiXtgb0wrJ3S2p@N%j9|3Lv+&!47PTb zq)~trxW4=Zzo2h60&vg<CKn( zo|P8A*AVY&D2iCp5W`y?w8A7`PdP?z<{8qmySpuXWixU7g9^~fa?Vw@z#%c(`>{OC zD7<@M6V3Z4^G-6Ggzi;S19+dmNqvtczWm%GmvB&V6p^$XpO?NH-U}Z`!;5LmpkQnS9a!`gz?iH*D7r+@ZO$ltk`6_)YPkD)g4^tg%9xs zD=`VPH4IH(q^C09yjcz&yyV?>c|sFN6Q&higu5GXsUh8ejMs%~K?>jr>ng|8Vw7Nr zS%%u;uwWX{wopzv>AW0wNY(gLL!d90hxv*YPeiZGDmBjQStwAcSHp!;KhkrMgOXVk zvc~virV3{Oa!~Bt7Wjs3cTUg=uFuPCx(_U3FVn^pLWzNM0wv<48|LE$=iqNYhFlLQ z!568|DSnUjZquvxc-C#k_Gi$@3?Q4(#nnSvztoN0o9(w9kV=;QhHs2>9}aHMEZ#oV z_Kl}rP%hF$*-H#9IHA+%)+shD;#Z~v%5tRUr$cQ@PcC8zG&Lh=B9b^a;#VC)UC>K> zV9y6(79F7_zD{VPA=vqf&})sQP4`s2AoQ>booqbP02p_rS8cu!Fp7rq!?DpU$ba=- zw)2BFjrEl(OA1f*Pg$X!w?)1zZ=paXbDQ$n+5U_b$B!f7UB5>yc<%Ez?&D2A2ufZI zO|AeVA4A=O?U<(2Xtd+{u4I&l{1v_FsPXS-O>I9Hz*g>hJyU?SN=8c-z+tknu|*`} zcjSX_z>V&-O}+umg68k}UP(X7M>**vbUTsjpjj|cJ%!@Gm4jBvVO~>d`#1?s5zzBD zR)$d}(X>Pk_I-7xgsc!(Rgc;SU1NE7Wwsr65tQ_71Av+P%7_AT^-Zd@S=>jjfcQRi z)}vAcas(VV59uC-4Tgkm=BVBgo$XpWW5LehSL`jX|YBB8fvK;|=?T9ppX@^OgR49s_9-){}ImLq%LMyGhoQst$! zW5ZLrKNPV9Sugm1c}!W=&~Xxc7EP!$U-EC~eb}YG-|2 zqj%VMze6I(zw>4ueNxWux1k|F9b=H+cbk@Bf{e1O5!$2nd^cfLKTuFj0Dc)5*L8o2 z42Z?B>x7RT_p9_l07?!frf1nv1y~EGB`5ReYNqal56nYER0!2%t9s~P>UbTlk69l3 zlo)MG*DCvBz5dQ)T=!}{E$d7qDa$G{#AEnkGh=pClH%qW?Ngp+-^8mAd7fg2YJKQ} z-_z_PujAcZZ8%6sO{w1aO-+C1@Z81{U=OLh+w6~n{q@W78~n9LRmV`2->9!}56ZJf zP8Gi#m>xd4#2SSdZ2^y@4kjM`UN@hz&GhOb6-(Pt7Zs`n?EN{#Y;f3ovcnCt0NFCN$7LGJzg zii0*o%N$?lE*qP^yuvsWi_>BYG^~%bcaD7Il>hX7$5d;Rpbs72i6M1R>WT&TQ(!N) zTri?cceP`{_qeWxUGK>jOynJAgAR)x{*y@g$FIbdgvQ!ga9FGJ5qDNXY#}Hp{Nqxu zZ3Nzp{>y#rB#X55F-THV4sCi03`Go_`J?Vz+R0uM5Av_m!moNeS~1LX?W~BLL|t_N zfYKFGhueQx9et-e=)nd0%SPpiH%|da#Xx`V*^b?(5kx8Xo85Rjl?p}#gMCcTvuQlv z=wz|3(K7QjbZRTc`FK4ywi{qlNX6@B9ciFA$Rlvj9&EQ6E8^DWFT_x@`_eVv#TSS* z2#lV=)0c?>EQ^r%2=pM@%Io@Uo&GZVIQg0sfuvpqpXiV}Ais+#^T&~@5$B(pZGMTX z5u?Lv5u+;_$H-CYi#)h)8^Ox>)rO;e;zo!0!p;=8KXzPNclFSb&u&vW#RGY|5;G1* z-r@RV&oOQ-?8N|=5`}cGt`XwgrVv>Q%_u9)H>PDfYza(cUnFpy2_TrLaW0z7{uz?a zUQUPMo)Vx$Y?>J#nA;})q1(%WO3X)N0np)WfFvM$6`0Ge7i|TT(`#!VoGlvLrHy7J z1XgJ51v;6mudxDu%zV9f$ywUA-X=eM+2%QvZC0a(g~bw5MoH zeqAsBpbeC@d>SZdjL|FGX>=<7D87q0+p{p~PGY1E%OX5lpWD^r}!m7{I>1SggOIZkFSV@2ch} z^~AEN3a;9vqH18}oG0m5}#= z1}biD9n}?G@!96eJ3KjK{7_l8S33LT1ak}u5bzNgKaE?bXW@3+*-bBa{=vGu)Yqga=FRb2XJQr`2@ zh|L}#VgpEF`Dmkzz_nB5QgcN{(sxoyvdx~jS*-;@Q>)LN{6GMDbiVRAc(LPsseE(8 zXsbK`c}LNnPnmxn$~>icw;)K` z=_3*TkjHb%Y5PUTSc&EZ*~a`_3r}=>eE*}-_SGKlU481%mvfqik>T{F3ExKctVE-f z^)yHCNJ+}E+A}i>rn8ykpAl=|xI9066WMvqeH_ zDL+*(u>|E6KR{lPcU-ED-cbFLa?uORb>B*eCmHe*QHfm9_|D5aq^$ioEaCc8xetQ9 zIoM>JU3|bSZ9nw2Xr)$B<=||(cJ$%IH&c(3jh3{1?mVe_p~l}Aznw~dBGmMsh6aFqd zU5^!;6o(YfUw~%R4uz7EF+hx#E*7VcWNdxV7_r?mh+8TWe>Dr55(v|xYds&l*ZF+H z^oiw|AZpu6-a(*hoF&4q{PTA8ay+d=*f~RTESR$d=}K>_+|hA{K0P zFT+czyR%&@NBQwMbaUnGJrrpNI^*ecjl|?h^Jt zjSXj}TzWZaOJ6NExb&ds8DfqSD{Lg~J~vvKT-*B71lS8Z2%TmQHV?2RET=#QcmcWK zJp=lCB;5eNy#?D|lW=EeY6k#xx6iio{5&UC+x7WW@=f?vmpQhcW|(hr55xPLhd2VN z=u~U#_k}X4fA>@OyriZmsP$@0fkC)Kt9R z{&2W|Okk|a`ayJniAJ_j6DiBYz|4Nhfj~>S)-It`4}AZ}?~H(NNksZ68)}y0m02{w zWs9t*00{{S!$=<>&j=vz&C=`Q1`r9yQVR91sg9oEoyoO` zm~&;FTt>-w8)5psF&X~Q74d$;C*+&UO!)7UZ#bWysoa~{|42Ku#;x&1^kUFA<6EP; zYoLM(M)UIZ-MM{?msA)|T}1YirxnzlUoN>kzjI*p&?`hKpeO~=? z*F4D%md=My8+M^sgVvhy63^=X9&2ctET9rb%+ zXC5CWRC#lFzV4Ast1a=wBUw8`7q1-);6P)#xrZ7Xp-PapKb3Mq2Y|+k#f;gAc`3bQ z_B(_WcRUzvm6ude@JDJWosQ?%#4?8{s6DWudF&hx);pxU}S3Fd5T$ShC6+dG>l({@RJUL zL9y$Rdj+w$Q-Lrw&SSZ_3r9Z%UvZI1&n4=G#Lq?t#6hJKxNb>kgAE^ZBW)TmUTAsT z@UqYzJvCUyxjc?71LhqR(Ri@%Gc7~}Af&kRX!vA6Yg{Ys0-?{jGTn-uIG$ZU+~EB? z2hk)3&`BPhfndj41YKekdPb}ZWVac7@Gss~JSJ!HdB_0(F48LF+PE$9Ms=iW>BoGO zM3hDS%rm_B3vbLHhI%S4ewoWnSuG4QV2=_bDp_D*ZqebX@7_)}8CXi8cUE|@`|3rWH^$LJ@Y|QnQTk7gPlD!Wtv}vlrHD@~y;ifV#9g7{lhj)F zeJ`*x{MZw#3HyLka!j+4Tl|%km(cIY7_(6oFDl7wzZW7FN^LH75F$E=el8R)cQx7W zXis1kgv9~AH~9f1|D0|iCdF-s`2nCmkhX8#f_f#y{~c3^nJ_T2Dwfg+ABcpEqQQ8X zF7JK2N@%{|AL1POeIt;TuKD!NoaB z_vr(#2>}yr?}MPm#XOCVUt2`G$DoUKXK_d(*2g!Uy_%je0$(gLJ!Ps=rRIM$mk?6v z-!aheC&U>h?N-KZSBok+N-`7>d!n%uCtj)qnerLNm{58e9?%XWVu7_oN!p=7mrM1e z+y^)5euW=XQztB@*OTnCvi6)90DdT{@&SshWAI2d-z>GGm>;fU)ogg0+@DZkR|op@ zAA9dtnEv8fM;E_8M#;X7$;FRNsW^+kZY;Ya6BZOS;6}K(;h(x9t^G!kihhIq!{{Ez z6M+SUyxX@}&-dAV2hXoR?|dE!Q6>vtl>dw;N~ibG{q-(04xPpLSV1wAMl0-b!v$-eJ zYqn9WO|&{d{(N%(4AD)sa}_SXrNiCtvt8H=dj(z`8Dyl$QQ!vyga{Q04u|Y%G;N_lMhNJ+T902z~GhsNpH_X`< zq>I!-kHzFZWBY&xM;3>HIO*9n)=-5&(026*IryRR%9LSMxAm|3kAo+ccyq=K_XX3* z-MCpA#xr(F-NJYo4t8EBJgd$n?n^(Tq$du3yRk=dB-ad)p_*g&99DbS~{GM$0pZYZB-|GQO6hsFZ@D;?9fmcc90H{{qH5& z3>FYNHDF1q6>otCe2E2%?5OgxQdt+poc%Zmi-d@?lW4|9lWov>mkReODAXQp1AlC+*{>xEI7q!z=>3)Z6=O{-|B zcX^7XpY#4Glsf$OZkdNT=;b%Vre^u-eY!P`@VZQx{h`Dwc83{V71T6sy;3l@>1(kP6*$ANcDaSJU zKAdKDXufmCg~-`E5{t{m_hQMNo}lXF+XAzF#dH5mz4q^WO``g~+d0lezd_4;X~jqX zS>^VZHSEc3N#23|=88NsFFj2=z$ZO|_xRkK_?v$o=>nxjwe z`?Lz??_)}DI*raMYxFvACw<(-gEk!KN&oWdQ^=uwZs^Q3P*(tN>D zZ9TuSlFwwQxDF_FUA4uv5u2x)#pJXDS2s`6nYUYi$$5AioWG8&GLereI=4rNVfqYS z>O15vZb)97;kJm&v~}|s8ah}~^V#VR=_Qf!ylRQ%X3GANE~43F8!VhDx~^d9V_T4F z0DE_`wt^t%;^rQW4PaAxzgO5S0gNL)xhU5hDSMFlc9N4pEuhedO$VQtHVn3WGz&=U zE_^c#Idb#qUOP>5hDej~+*7_qBhV>%fR=*&IQJ)GxM<0yMnL!B^3^5yR85rF>TRp@ zJ3GyI%hvyCqu`O97##iXPwNnP)Sb6%r=yQ3Pn^xfxnic(^G05XNU(ofs6oF7%6`O! zKNfPXd9p|eTMz4cu+onl_1J}Noe>TBDo;M3r5T+tS?AKHt%^&|o}H`IMwcT&VG2hG zBm}-(xjz3xLgJMHzHfq;kOnNZneQlFbk^}}{1PT;$@>uXtNHlT zJrM%c^Ss90A4}8sq^)DXE$R8f3HAJ@J??xyggUx%wyb};v@FW9@j>D{&k$I+XHn)0 zk5tZd<3mCt*p{965MKF{BlL-I&ValEzR2(pyID-5>AneeU$MZrRvxE_+CIdF?kLahL83ap zr;=};8lF9P!w$e59Zl?w8-Zztz)(=ozu5G7y+DS4mNgsuHhV?htFN|Vt)>E^6QLX) zaRUj>9GPdIu@Cnxzlq-X)!fU&t0QE{eKc7+^~LEx-F1&6M(ITNbfZt}@QIH}LXH`8 z`gnD-z*Q1qthHU!mEf3)6v>Ver*~0_^bnxZUU11PuH9&2}S(^wT4H}fK-{%s$974~1 z-g#fZQ*Z>_S*idlilL)C$G!APX93NpmP+)>)F=Sh=cJ9ex%=sXc3=*d_EO4nUVWxQ zTWX z*{}3}5p`BkadpAA-rcxsaCdjt#vwQaf_t#w9<*`yV8J1{yF0-(5W(Go1}E6<|D1cr zed^aeM(@2=)vP(c>h#SUT}2Hq@j>U$EFOoSt0oK)t~8DyU;z>Rg02Hl1eMJO2%OkM zZs2=;W(XSwz3Hu1yV0SedW$T-Ro7bP} z)uxRlv3u3ys)i^eTwuU6xPTaK^e-*dY+>~88#H?uM4(@YclPGb-8>Wf^784-mVD9) zpQXjb(mUz{UV02F?&aRCuR}P*4xhvcEN$$3tc;^(ZD{#)5&gjwchM+%Ec~6e{XU|Ke^mK zBriiq@mwjdC9d1esBcFz-E9bD^LoM#1HFYJ=(ysGM&&XX1@;+7Nb&L+O`5q*PEs*k z&d-R>zO|1Xwn@s|B2KTe!hQH>*A7PAj}Eb_kb-w#y>|o0+b*>J2&Fo@uB9hu>Rb%o zmU0FR;LP|{%%ngNumtP6@A(+stC579! zyoleVUCpx=Oosg1)(6cke5xeShxw=U(A^9=*}R4Z7LLZ8m1gUwYGehz14T|sGr^Yk zhT-=O_l8V{f=lyrG$swpo}c6s*{Tt_(DMx%nq#Ju8JS7AWcy1;%zC@2fBm$O>|aSL zX=F+w(qT)YS!9lWAYO6?O@XK3(#a$dzri!XLy&&I2Brh35I7Ye4^86kZ`cH&QEo;f zs7k5pY^Jgx9h)=k55Lm%R(gc{o8-fy)||O(o%SBH~^JIP4>cdN+Ib@#_>f? z8vp8kM;Yl!%$3e4F;?3QKwd}V22EZvCJQ$+j+^PG_YoVFWy)goim>N5PTEoROwHZ< zlRe?`Q{7>WP}41tPC?htvm~ep0cmuBPugtl;oIZc{$?2RH*xEXHn`t5`cWtnnA$en z+HeJ&Xq=rafC|GV7=5J2K~b#;ZF2zD^B5~bA~i?=aNx?!jgHl5ThOfpdth{^=Qp-- zxS!UIO_wo3LJIwR}&DCoHVL~%)78+X2 z$@^r5s19_;Kd|OmjFD@2GE7V`UDe3^aCNv-_&q{NVS~sbAKLUm^BPVz*dZ$m!B0ZhUPKSAluL1BuK7#M!hn@eXimL z=H_HtRta+HNKs!-BV5K>Lv$M1aS0OM&tgCR7L^jHx4i$=3C6N6t{us5(A zwb37*40<){{bQ?mc+{FSVB;k?*87{Mljj&WS4NBz?q=ku26qPGi&9I9aI_8@K>fcH%4TeFNtE8v5@MsDY`lXA`6R zr0M%?(w>`G{!UYTF9Pi5$3gt*)f`vR6-T_L_WE42EsTbL(6w)DoEFrvE^BcVwaL99 z^+D$M&J~YmrXvSIz}Hx<0>(<)6%yT^-cgy%g=%FDXVy0uLE%go$ieU${pDdvkbDRj zi;5ZqD>G^clgSq-Em3^%o;ED@cbAftt;crh_yBQDIKa$6$i(6WdG_VGewwJk`a1n} zB4ACl@k<58`~>X3-Rcn~cVS(tT^=>5-77*}S&G|RHDVPA6WvII$F-i?(T}@QNacEL zVF0#KEn0h|n3~jukLp|@h=MZ*Yag3b23NiG~N*Y+d}qi1?dp+p0+ zA&hp>atf}3P+=(mpDIHE1P0c-@Gj!H{aRnI&ryQ{&pOgKxS72u1^9$K(?0h z3uWPy(9dk~lFO`>-oMV{5!=xiVXjlKZY8QKCmh-3P43^T6R|(RSK6!%j+-MGUgw;~ zJjWl81yF>S4F=gJu$@taH%8Ozv&^?A6%>4huBUVo>%chz2%__u^G!a4YEi=TTybyZ zN*X&l*NvtK)|ien*X0TeKcjdMq~aoN5)Nhd!yN)xdH}c=mEs={MQS`3)Lo7}3!zJ? zwdVKU_hK6`Fl~!NpA!V%P>ixP?L+bJxAAEmHyG%rJToOs#q0Z5qDP!X&e>C;!$Y#B zvWV%I(wRY~+SKecOPx;f&-_zVC1u{$6@7 z@16780?hlcrE>BvLm|FDkMB+5g01rxj`<$U9}hDuh@-XxBXQ0ecBmE>{h)jFbm>LG zMg_rTaL9lPDkKC@feGjm7+2YiL{!}1Oq8w`uEv(bF-UBIzNzee;{C;P=XtK_zdE=)RP?Sh6CC-@oFku+-z0L?s&CJIhmFTXHwNOBpWKss+&T$N;ai#7Dz#F zr>Wdl#ixmy@h4OQhmcRdO&aR^*DD^Hfid7IE&z%GY7o347#pB?4j`*amTiEhiZxpg zI_;j_o*^UIH*479;5^V?S6)rw_4($x8F3^W%RQz3=y20iJzM_wSB|q3nncUETF`0~ z%NAm~#k^Zu5G4@a8qdLMUY9Gbl{F)|L-HwKLw+)dPhbIP2zmDz?Y~S3#G@BD+ zeZDVkj>4UrQJIP<7)ljowPVt@sYy!~35KSfQ_4R_YrIX?0Ms5d(_Z|hf}B8cp(I=W zEGUu=lhr6cmK0I}h>D;9L=KSjqW3OzzR;y06wLRdpOnrUc{LHo*>XoATfX4^qs$*dw>9C)bzlAdJ;FTYv{Kbgy{C#fhfsGiY z%8T$3L(U7g;b0x9U;rvZB~v9)((_@3_lBKe6cob|1V%UHf z0W24`w!%k9hz5#B+2=TESVG$+UYbV+Xur%C$;!SU9}|yydsEDl$@gjZ`3fn7ein+O z5N8uvYsi!vY%+2&=qVJa3T(YoiO%B4kd&F4DP!GPI&l7r#JyX2Z1N?MW4`geWx+6mw%bd=k%K(OgK_(r0@moM0=Yo8fYyoP@LF=3$+Ve?RlAqG zbGIhT6ZespHFy(Z|6Hd&OyRJ6K3#!SzCJO$nS#&{YQte;_CNUa>Pbf4Bd0+C9{SWftdPTeWRx z|9(^@Ckfr#ilhBNwP{AZqv$$z=X*Y%0Y_SH$aLYvj|)~u3={#9n{uR>lrCH0LV*Og z$bG*EVfSNbu#;u1DpnIUSuLafe5OlqHOWDO(0G?UmwMXy;FQD$gieFfBpKJ2q-YTO z*P^!HiKSdY;2j-l`$t=dAQ_jwqSQhbAfmAe!i5KV|DAq1ZjMn6<8ZK>TKJ6tu48p* z4K5{;0-@U7!q8Co_?1m{4)nt%&keco` zM$R8NM?AKAjGenFalkFu(T%!J8d4oe@$21}T5l(_kRD8hXM{=V>}$s<@?W0fMhNWt zQHL~g+%yic@B7r?87%wu=-zg{M1N8`{qC~Wyhrh7}BxGBW(AH*yo&P%K;-Y zH#!Df+qZG2Ud2)Y2S|}G${G1Tsu`yk{unvdWZxUHugBVoT&%>o_WG|K?K~LC_dR@` z$%sP|bQ0w_hSSE3zJ9j%KZ-mK7r$OA`QbJaVLgI_*C_b}{xBPImEbQpGM`n!%fL3W zu-8ndRFz_MjCpu_cq4zLmvJv^PymMqVnRLagn-2W5;lMV0(zE4wLSAjP-!FY zkx_X_EKZGGUW~9K8*q(S75L(HwR%UyKCDHn$uOqy#;dZN&IYkI28C|9lYi8MYa03GQ9tgG15U+^G-q8^WENzvfdlpDtoAi!5+y=VGpV#a~IX@AKRM9;!OuS3)$+2V-V;5L4 z$?v(9y5jmg^7zW3Psm*0eArY@tP4`1nuT1chlRS_U& zU=T+G0D>VBxwt_XW}rZ}Uduf7i4%AYMVJL6nxc`Pm>?FhU>+X_XZ(_OYU#zCNO~;M zWG;azxJ>y#UtH*u)(uyZ87J-xCrb{q*AP|98@*~WwhF_R3HWPe zi@eE2q%>^IskO(STY;7!>*9SE@%_a&zqL|n-(&sCGZyE`xt z=^Y|mT6pvAiMLXq-5N#NjM+99QRPq$IAp?rL_$NrXfARLDgXl@q0(zfk&oyQEKeIs z++fHVPq7_RYKJrOyFVzbwN4_d?C#eHaVLvJw`k04Vy&pD! zfcN_t-cd|5%7)FcME%_l%gbeUsh2WAcPkp+Dg_VEY7>G9f$J56y(B)_+Pz|n~?|Nf+ zymKnQI;R?CiH#@;cc4VRI@%aGZIDn<*|j&N%U4iS(j>WDb_FG`V;Qeh{HM0b$H%_8 za@UL+hA(SW&%+bHIJCZ=tb3AhC|C%$c+F=pX9L;u_1#!ybP92sZsCZ32?l>x2G;ss z$I?80Pg!z`>nW6yuJlz;hEjjT(HLt)8Q=>J!IB_R!NL#&FsJ}nD0t`h6wwlx>wZAx zcl^4%pfJ`qDjIzAk)YwL9Lwlw`+N@eeE-LmZeNcnotnAPVv;ITxAcBLVnUf|yO&|k zq2(P#oHaX}sryN*HO;jTgf!g5XA?rh1wkVn?qVV6Ln`?>HC3H`-@B|`Gfv_R-R;#&_kVoHM(7;ge(xvFmo-Nw)icsx6Wnza zb!i=|k1TMz72fAOdN#Yzy!T#WZg>dP+EIwvx9%j@OC+tDZ2aI}-r3BaTv6{|Hg>re zd#z6$QsqlJ!0D>{Yh};4Qj_q|T~0^Jl9Wr^G&~`M6+byoon&)<4)7oWlKB8s04300 z;@l=Vfjp>;Ff`j1gWP%oQw+wME|r5vo22}74XY9$iQ1Uiv$LLUW$G1`v55~Oo`HER zlC#6fqF9;zd`Ke%MA^ORGiN4C&RrvCBC}!?&Lf9$h88Kqgfn5@nDWt!In0se}GUYLQ!zkv@cHrzK#bB)*l(khfXeTND;xt#7r`6KE zi99oWPhtP~x`THKL@04G*Y7$!q2)a>8=r)xa4|QX^mnMa!&&YqyZ(`bJk zH#LD1Mpr)Y;(J_nfZ(pu3T~4ikqGir+#+R6V0cg<+nIj~n@}ndaYo|gDp43x$$*n8 z%n$$@$oZXO^xvR3W)Hs}hdb!Lzeo~Iu2H?nvdp&COu`o1q@Tchd|&|Zyshc z%J-ENOs5dr)u}IJMDvMg08zo{l_a>%1_s5v+WVTDr%uSx=6AX+7-U^@?RYkiW!deZ zMYItEo-iP?S~Fkz;UM0zj^zbu^@*u1tBLEF8r3_vc10HDp;OMDz-g0H1p@LM0 zMoXE6^zb!BiQ)Wkv%GlbNq|jrHK0HaUUkP(o+g>3cT57YuMNwTWV>FjGooxy&jh^A z0tZ01ES>EI@VJdWkjjd=pWG%N=Y2DVTac?i^eW+#9_JK%^F?Y+@};8m`OlZU^yfe} z`c}JMWSTH zSzI~#p@P9bv?K9lXI-&z%#^y_42dQ@D_T2_p0Hs&%<8S=%>ir2w`OW4Rx5V~Xvt>$ zz^?zxGEX`r<#?bW($E#>&;tN(>?RIf;P7DKeGK`^YZ-YLFLS@lRrX}|f`w|1I}@g z`jEgU1!PU$@1yzwSy}1jiZ4wWh!yjSAYhCe)xYepz%oW883(7Uk7A`qgfb?q`QTZ- z7;bYn(1Q#raT@?0YKpSS;P~d?0fLJR9wQG|*x8UFaR$J&sc5E8B>d zA$;}6-Svs<*+tz;G0Lp!dY5rC0!KzY_Bo=XBT9d0L|8SY^y#-ZZYt#1W2MBN{fYQ< z1*5@7z{oC*c2*{;xEuDRPYkn8!4m)DzSOrCl=Y{522D zHB^;|qv~=0Y8)_B2`EQ;q9+gw`ieB)Bw|Th&{~NC^xGn=a;{Y^n~x6FIq$sao|^0| z(#1b#u3{7A|H#mKvx;9J1=_(A@w>V?%R%3jM!rX`hvI+blob=+Q1{4D7m#}&Rq-g_ zCLbbBKIdPnpXdZv2h%7&H>^o)XXHI0~U&)C{V!UWIdy5 zOtM_!K8VHP?RzwsRgr3N5G55j#J#W^xxY_*H&tAdO=|r)-R~U$+!k;>x6RFdb56A2 zF!^@mkH)mD>4?dZ7PSUVm3dV-qmStj=24Gd4`rXKs>*%;#{&^xu?ZWK^iQlP3Kp1J zL}ZyigTPi_Pc`%Q&n`F;wxK`H;XTEN4$xw7-=VlihlC^Oa+D zP23Tl()qOH(#-toZybr*h@YA}4in-V6xL$Gr9CoVKL*LkM0Pk8p0bngU)xdOgmRGg zJ}VEQ`g7S3qe2n+=0IN(5DSXo8Ub8bkjJ4}FxBS&-Sz?X`yUP~@Lxs)X3_rRDa)5IZ$xRrPiGu?2;>UrU>-UQj6(=tNC)9-r)UmY7;+!09GRhLAP&`nsiL<&j0kF0@^R*^I~>*ITlB&UIl;3B;Vf(bbQa&hST;h^kL0nnxtOk*`+%a2GM6E1(b zJzrSviz#}gTF_4+#XBbYWF8{jw;2S#JLH)~zMOO|WSsW0pPSMdY56BJhT}R}NMOLV zQYhMO=(itC3ynTSZ?2>DgeEw`F|KRC=q#0De*`9<@HK za3=>qd7|oPY>imIU*j*GFO-?g(RmpY5r$-=)BqO{fR_AvkF&ehMoFReY$3eCYMS1@ zG^xw1c~J^zGzEtQCQ%p^%3+&w^lWGgBC?+DKY3E&=l@ANe?KJQ!OgFopB(`GsxJnp zr~rydz_*j)+$C~?ZcgI?zTxVJ1l?{0(1B8l$Yu^_cl&1(fX@fmFt>>j0g_;IXbQHC z2%TNh$_@c9TV1bAojKNr?90WLEypbL@X^e{u7f^OR(8W zE<6!mYPmP&7oBqZzLZ2u2H|ZQ!+6Ce!{H-FKR*eRP(Lpt1H+*RVEmnt2>~X{0U!!C z$q9x=%n)a9hZ9qp=x8wvzR)v*dDvA2AU&bI;_SYPXHtAxew~%#?D{fH$AGR(rt=jR zDy~maaB~e(XZz)r5Zz~B>i}m~0yg_D_8YU=;?w)jg(;bxcofeKZ0C{a=9@C_52i+W zt$o}uw*xfNddD&`JwA5hS5;#!&sNB>0zuye=~0bP*#NXBxVb_&n7nLq(yJ0VCc-AU z@`TWbs`_lUzdMhOChliLOJIfzsAxP{CQuema@H>8MUuRliU!ityALB3zH5;JK?$iK zaS^vVV0;2y^Z_{i7#8B8UArT~R;^`rm0H6g{PM>{507s$lZ*U?L=h2}wz|!ebB9C5 zR@+8rX6Z#;!j}DQi)Gq#mdist0-(9WK7f8dcnXsM0XixGQ}$qhuDgG}S@wHC zHj^aGmE0hmXZ$g{bp^02lq(N^rCs#>;V#+B9TWHA#gXIKzej07*E+=O;_M7LJoTLo zoYb4af=(fpx|)Tq!WYsO<$!2cg9Jx`g92fJckBkh-npV@ zk*aPtsc!5@<8LuqokA{onpe78_MO$U$mB((?Sz&EYUAkx>Cg*hw0G=SS))%lMLy@A zwR2EK6nR|CG1kmM6cMoa*Z}v!U_?5Y01|t)L(O}pwEWQWyVLg_m!+E_ZVwe(OqAlx zy@DC}c%Jje7ZQobvsCSjbrVUupf!C$h(t`^E~*`e03Y*Lb3Ipjby$qWuyrEFI$Fgyg9015Gp z^i-x1$)LwqyU&8LFl#})+*g%bH<)4V;_v8_3{-sdc`5r#+aN7G8B;%ozv$lUXdT|N zjn3aW6#Soi2~p|_(%(7dm|}^H9zi?V1eNb+%P(8^I^2KP4;X}#`;*WLQ zSj^NB2d4`v&6G$0q3GtP6qt`o90;7uTbA1$sYwiwe3RFS8=;?KxR5;FZQu@qk`~O- z#M!*S%+tN@Z-a<7`i%KP;5KVE8*-XFRtem@IF%q4kToMO^F}^a+YEXIr4XN??Mi|l ze?Ogz7;Nsbhn)<9dt3>Z>h%RgLyuz&uTgrk@fSZAU%r($h@YMjG>pC6dTYmR&c+X^ zTP)k(@Bds^M=eA6sNu>x{hb(`L=0>rFhCbTxcFm88nCVqjbeQ=uPky*qG0{!!{WW_ zoXMN1oNnvr(<);;QYkR3sAi`w%5)u&QpJ%JL%TFSM`~iZG5Bwp5ZvOGg zY&2chB3}4|&ujda(R|LG?OW3qwXwx?w%W*dD4%WK5;PGi*^be_=omX5I_hAd`@iq3 zq8Pno-rOGNX3HyT@^k)L?>I^++6g_XsI?o@%cI@=cD|&N>{)ed^DRsh_Xti9X-h{cO7xHm2ChuQERXM;nAT*UO#*LhL z1~LP|e6SP*wzy#6KM0%S$1muBDY(6dbiQY^cg|L*P%+O{0{l0(=JUEY z2}PV*6?mN|m|9ByNa39#@ng3_amaWhnkzE@Wlp0=xEq}-6ayz1SCc4JaMQL z$RmU;_c0*!ucot?XHJQr0&>J=pg9mPF0kNB2SuO|t-Yf8v^hO;x3Q_(qDzeTiEE)1 znN$TZ`x+hP@}j`f>?)RcLS1R?`}_Q9?Z7VRaIoF!yFzf=4>CyC$KwdnkHyqcl**Np zAXDqzq&uIRyZmX>2$%LccL8fj2`_ie0QCR@LTXwHED0)9SyJmnFJRqir;C8 z8k1;7hz8H{DyAuHdA(WwJSG(6IIuPQMj75(?%C1+=_h2-BWzHcw`Sm8{MN!?m~2HZ z!L~6fV}*G)`%fbI)ynk^UKXz#$gJ=MW}GffxNUFuRdUI@_?z8#HMIBl^!oxwJh_OT zOCBZ!X@(XtdaVhMTF`pD>?8k(o)A+elL$17hqR!u001{1>e(`vA|kfWq4#Lxvooqu zeuA4Ku_|WLMEyMEK%NEtK6`vE2V(1ocL0M7LcvrZd|1YZB0X}9ET@m(*I*}qv3SJ8 zqRu9^Kpj$TgaVTR0KN;fEXl!uf9A8~j}K9rSbwY|H$6qy47W;Z@&5>EeCsxI)j|vz z2WgX+VokbzMczsx&@jON84BMB**U~E2Dy~HWPL4&)ZXywNnqWqj~tNnOMpawmqUvW zH9=i*G*f$4s6YfE(j^>}KG zUbtvM&tIrcZI^%e-l1<3k#{mBVJJ-f+$2Z2PF3V!#gvalN`T7DrDqZF@tET=ET}Lv zBN>Bh9WV)?vBET>6Z9*w7RIl&6YIfpU}u{~@((Li;5 z_FVG!!SA*>1D{4HE6NUPwQ$2MeKR|SUhajS%xrMNjZu$^&RTID-11Bm261}!#d6pD zE=*>vl52pX^Nwa!*42amW9m%+{;DHELa z52alW>{--A?=STWKvOV|i8cpQLCIEk#Uznzxuobj$O{UHKoCP)Jpdhw35AE%bLuVC zR2s|(MEWN~)hUr`S`NqYneu?yTgQ8}7i4plmX{B`m)76x)pzQa2O7FRAgl&4=~7=y zpOSH#1hBXn<~Yw{J?3QcLq@4l&Ww51(kR zZOGwQSUNPCxH^YT-C{pB;!>O5l5lV4g{t2L$uJAfegS= zkzfFaCTkM;g2`Am&$;viev|*g1bG<*ak04-IspebCRBOegCO{yjOOt(I-)Q6VBppO zD6OT(>gzt3zqO951dT3Oo@_2W6k5gUIWAmg{_E;FYM*})dwOb+^k+^B(b~1uVe6wj z`{tW=;XbfUZGj-@EK;k|hD%S|zvQ`K#WM##^@AL|diODiP^eN}N$Kd9b9R&Z67$FJ z|9;RLTN?9p(b$3*z|z6LlFWh;L35-cz#J7Qa3$|ipU(21Wp=0y3o;`wqdEijR%Rn3 zN1 za4f>-GhD=SQEhF(XrXQ#2|KMXLC9|{CFm}rN=D(2D^dnN3DcnCX)RT~*$n{L<(kVgSn`cg$2sC<+o=;Iu z#{Xj{j9!*0aJTv0*J@hdGA2yXkXr1@)0i;scJB{tnx({4QlQVWu&_3OTcC^wRYOwj zoirJRJ?vzD1KtyE*z~YTEKNNRDI2Jt4*DL@b2EpPs)?TrFk~z)6v}HyX0(5-#Vx%B z0WIkO*Gt7Af?I|;={NG=KpGK$`sp?%ZD;gPp>jgE!dO)p+Qg##sAUweF#lOV+|g(B zmc)TjW{wYpJRdNr4_H1SOMgHWB~8Iur{{h@ph~i$U&oo#z$GP4E&?EUz?`gHpp{@u z3Zq^X#)<;u8e_Uxs@17}r|X6hdN>Y10KhSj4cnm%9w2*dO|#grC=~_fYJt_4a*bE_ zkLsKWzuR@u#8%?c^`8P;nI=Vo4klmhBhw_=GPH|Lqpp6=4U1_+s$fmN;oC2-%tw=L zbj9v*r>+_*?KB+6`PQ=`sY(#{^lij0XK#5@2SyK_@j?>7f**PGJlwUqiT>3mstL|) zzKXot>T^PeN97{TBsc_YVM&sH9;sjk03(k*aJ8XZfq@A@EnRPfZEV{5&%li5$Gy1&sUBuo^aN;I1F9+ruEIZ!gC9E3hMYT?QAn)kZqGMU)QlUy|XN6P*{(ySu_^no!>Iq8ueP(>$&n~?je0wAM}Gi|Bbzo8XdELMJy~W zlvjc!m#5K{#>L5RoM5AQ*}saZD(-t(ch_n_NWBhZE8Xywg;nh0cIxzRh)m&RBlAj`Ob@Dw#pfclc(ZzcGhcEp_7H{S7 zsCKvJ5|Zm3=&~3x#x3`;m{IvRVZ_5|1bmp_HV2~X?UMKdmBg!OLZj>)L6N-u750588H2^PxE}6?g z!}{{HH9H`>)vj`~FH6?igvYR@1DI9r>jk|acQ)lX3YlIJya8HaA@(a*%kDdqfcRKj zr2f_e-#Bb`@^`Z*UL2<_LH!le1x=gjI&T%G1nfDv9YzFma6uE=>(p^=5lfG3@K^Ea zk;qc2;;3TL3EPWy;!GtUA)J3+d-cP=zvo;gQ$76(9|6V8q{0JnfgzWG*(KnDZUoMS z6=3UkGFIQRXR$YP-Bk7uc3Zz%avB!yA)rhQm1Upm7TY)8&8)r{*MKWRaY-lVdvaY6eKI;L( z;~ zC;|eoaUfQ5^ep8QFhlB=<(;BHXBUcSU^)*)yCwxnBix@7=SqUMma`((&Z32P%w|Ws zl$7znVH7auJ_IS4gI)FjeA97jqZzp0N#fqtlKrP0d4fMtX)+~q%cYsAM_^%`Nm{$J z&8RAZ8u1B7_?*2|7#IKVTcw+yAOvPmorjt9Tp4FoK?5|}!%BElK;nma7kmagO~J(Ev}D~IsSf@Ipn*I;SYJIACa;MO1)=}bvq0vnD zFL^`KF<#+O$by9(Exz?)vvj;H6(8flVmAIyS`V#|#bx(W5L*Oe3SoUg72~Av{sfe0 ze@Z;Z>&yvz5w$Pe)m+UdpSgrIK2*hAW5ZosP7ad$Jby*WmFSGW0$axAp_p_}UUf}d z=r2>W$NkMZ5+N>yPuB2BUmcku%v!1(l*tG9NPMIiRsH8ucEAvuTk(gImsW5gPv#g7 zEuI;1|0!`3YqaB+x8zKh!Cm)r6J=1|Oi0=%YLY|23|6j-jZ1wm&u+hfAo1DSzw=yC z1gh|r3L9u06tSS_Dw8zS-ZMqQn2)?ti8#>{Tz0gv)Ql3>{JJfR5xyH`vxA2`l|7+k zv@4`&zUzD(BJXUpGb!(jIA}*|_B+nE>1$xDtZ*+Fa zTwYf8jySrcFJ7`B(TxwV2^4|gKLZ5aj+x=DhbtHSy*t*GSob>HaKU;cz}=oRxG zz^Zw3em5k3JLY(A2p>NS8+ESM?tl3pNP}4bFF5^J3_m_j-49m8fV`>6&q@u94oX06 zcI9t)z>U}PtHNma2gJzLSwW9Yf9e3XUQ0_iJ}OUlX6rN>{{!z*AzMPPkxGMbm#BbMNHva z1S!C$7C3y$bvwH_N4fDo@# z>_?Zhh2WhU!u{91BLobkg_|IJSeerfi$eHBUwg70ga7XIzZNKH|L4-{BZy*%G&F4a|b zPuR2Y_n&swrCS|>Q(jt7xV{WMHGeGuA($l;L)Ez6WJT$3Ku-%;-yE$y1jHXoI@ zC=5Kdb0H`UMR%pSnwhD2m_(&xs|*8NbBo45j}@^15NhcX1l$joGU5fiu;!1Zj)@Ob z?!A8Ak@dt|cyMx)QNtmw(^CQWgSHZREq9UcMa3wZ%zLttivXw?D#hO4uY7qk#g%7n zehe&&oDuVx#D&qBw~3qeq;C3v<{#D4bQa(Nc=U*{KARjZY2!He8!O`SNevwTEPQ-N z=$5XOb=tGJg*~Ub%;C8_-9W-uy&Ne)It^MQzxS;R0BiyLITZlU>^ms7&4g^DEOkus zx3hmf5u;Mdkw0U{e9l@92?LCB$s{rISs|hS`N!4zzN7ka2Q1^o7dd$I{2(EfE}rpg zKjsXnAU>Adp_>_HZFH)iX@`cRTjhp4itRK@kq4n1gequcCz%U$HeY#y1%$mx%XUF~% zq4>F{RK})ARY~w~Jif-Fd5gJH_fbQDh+455nJ2CYM%&L-Um{j9p-F_Q-ibf*c@3T%(=T3*G&<_S^-Y}1FUtJ%T-%{@yWwvoZFRLZ zXJnQ!A5i#3c3bN|jn?$>I_H&!lMgwNd(b^ONhw?SPl&RnzLNw)5mT%gV_sc*iD zm*%TMj-Rq0J{UJlo%SJOl|{dM!gQmWGUM-F7}lEF3_cV6l7P~`o6-;l-c3McYe^9` zHN^#r$Fcwh80%@SFgOwghI?XfIuT0a3t~ktI!tZ|jwJY)QNm9EsA|vA3}z&{$Pnae zYOMw?|48qdAfoUlO+HHxU78_j{}*~Wdzo>=-%+j>i2VLa0%?v@LH2T>)OT9>OzAp? zA2VPEMQ)axI>Ak+y6GQlY;P1o>WB9q3!wjo*c3h}@D*841*fTE+iZsA@>IStm~-DXPZVvDicb=+UC}2E z_$y8^Gz6x0oH44=h5iz@B1e5JMUm~PF zW}mPz^RlKrzM@Z|^arvy)G+zqOcGkC8NVDib1uIBvm^52O~wL1EW!0=;0q58U|mdd zI#QAHWL8ZgSsh`(GH`|hg)d4dLls~EL8708UT}myWn=q_qyI$|SNM4l@#90Z2LymV z0TvN64DI(^-N|=O2_MNad{GHVpdFhcP&F{5iOEOA4mnqZ!J71=^n=(GJ9n^tHij+W ziw%`tR`dnHnf)Os04^T{iHi_cNBu-*2o#h2r({K0KHKXWQiJ^t|LSxXHseEZk+c?b z3xjNJn!qERjtCuGJ+}S$cJ=yJerW|I=vsmB?)l3>hr4{Pvr}|*Z4YOTuv%3Kj(~2f zvId|US+Zd0#RURmAw!VJGa;uq_dmX~NBmvDnuACUs7Hy+xH$^r&1LeV0v_2u(2r&y zFdL*_8%hJ}May57`-(YdqaMhz!@!7oL}me?=}-0zpLKckAvdywR2xGAQy_KaAF-W|U3D{o1i0 zLnCUNS^B}oFkFd?SMsxFFx!14QyISv;DSn*c_>!GW!k<=|Rz zp%B&zfB*{P2;iaGQu#m`2Ec)<>$e$Wf?wBtUP-!(Yh_V*=>>!JWC!U0BMA4^0VVTo z-yfwa(XC0&u1FbkY= zq*L-TiAgSl#s}lPy`uN2a?(VjA${U%&(aZXek6X>gx$(YQL(W-d#y9jwS_?m=@toU>9H# z8Whv45P&63$RyKa70Jn9`xSqVq_Ran2xpnb8^g{PDqNab*bPlqyL{KGQ}}-ZI0?u0 z1rz8F06@JRnD|HKrIGiO?>BxHOlrAVI8U*BEAhl|uE;oWipbopM#oeE5HK+(4GXPN z=te=GZXr3-PufknC%Ya}nWbPD%v>bwlOz>HK26dQ#Xipjd|@ofd5;W)0$>Ozv~Ge< z3m!mubQ1s%@I0Vk0p2`c*Q>}mpg&vjHh4y-G2coa0g%iMD*-SG07}S@lMr%*c$r<- zN~XT&ZE|+Ka<4j9RSEzAh=*06`@^dk{Um5yMpQDjmg7fM6aZ{ZLpB9;paCo>NCIfU z0^pI4In@}%Boz~81AVfPBUkTv#45l~D!~L30GH4$n1F2n4H^If{w&{?v&bA;|DEOE z*iQvfWrX==@;CtOM%)DeTmWDsXgOmz{v)_BYP;HE1GEfAPU(dc;uI}8W=|1IM2cZhm8a(Apjj{2mlGdgvmHO37$RQmy5`U zTc6$KR``+%6Xv62-W{9<-~s@ZK%+nYM`HN*&964OuY(441OS;HfV%%ZhnMR$qrAE_ z1;T0N(sM!+nqb^h-9>_vXD3{ut!NgDUhV-DE)jx`1kfH#umK8KK%jXD!5&|RMB3<+ z##=zg4Ep5cBUdc=!cXJ9py>dxn1E-3{Y3{L0a)k&W&?gLueR+hiU-AKXL+K%8A`=; zK1!aXCri5Pp51W)Z~=e<@cA5$@sHqX_u2R`;Rv5;uX%_^WOv0CcMt%0mHpQ)ZU@EobQo4ManMZ$JYQC;%V;lMVolzCc3<{tI9FN#q$EpKW@g zya~$9biSQDi-lBx6acsYuo*z}rNlqNJIYAxZ+y)CdA42mvpIgE&n4ji0PJ-I0Ow4l z0#8bR)>*9+j#OJ+0k_qZxK98SpgjT5J&1uqn~J;5_{?ku5rSfB2PVn{2giBRE@iJr zA*%;iKxfY3;|Yv(v~2@mJU!Is10G1M?a%Z`3lIP%Sb&uj0G`X9>XWSaIyAmtjhjA7 z(3m@ur_q@)S@ehmfJp$h0$>CL0IYJ6<3~!WsdG|TiXQeGD`R?6Ew&8Ax#Oy$xyl>`S$?iz173YY@dj_&e#0+uwznn zI;}MXs+!d&80Tva*s40i5{K7o$b zO?M@d9a>fnuopFm`{_IRtcS1@Zn$7@=xY3a=my!a>GPHnV%yeLw8F!)7n@gVeLVYZ z!LdBDu&*N(9>@SX%{ObSl83@U4ZNjj=-sV6H{-ho1_J;lGyn~tfCg4e-p6GGXD1K_ z7s7KF_M~<|3aMq&0LLH&OZ<;uUcW7u>Hkd6I`@wd{--U zX9QC?{$LJ@&ZNW`_c1)pI>EdxNn_iJr$4F{v;YiH7XEW zAQY~S9aE(Y<{Bh6cS^=_B#f77@w7S`XlMvlWGoV>=g}MJok0*CoCuJ_%jbdS*UN@$ zghVqZ4Um8dI-r0C036-UJP!Z@=y3qR1-V`K!}@`g<}Jhs+*hTx2D7On$Atj}*trbZ zwpjK*bwEl01khJr-zk{sSZU62b2lonFyXRpIZjHW0*dEH02Ei^tMgS=SS$bnP?q}I z(vqT~x}aQs^-!VjHms}E?awJe@Dd#yS5i|S00H#9IRJ2Nr5H-Zxq&Lx0jPS&<=bZR zdPoQ%ln!Mw7-jSnCK~!}d2A3(0>cG?S!EBjBnhdHGxc5&jxjnG!T%8nUyM90*=yT4 zG4Va8f8pODwDEc&3Vr-D_L#f(cr@ShemY}Eb-kaSo^9@S9y4zI8Gijn`Nf%+)gqc; zK6*?OmpZ%w^^=R@01hY_4t%^vP3hr8&x~Xf6vr-{M{nhzrpe1B)q_^x!2WDPHpt|o zU-Uv4n=KYeJjDg8D~O$ih8!ZH@y!A&c`yMaqz9k@CPBg}AOV00zWY7R+i6uaKo~?Y zE9?d`*0#r;@*Mr1Xl&*ju#L+5S`u{)#txxE7}& zW|c=`o2fWutZq0*#ThS@;VOaljWK>E!Qdt2ViRFl3D;iR=WSd$_trg73fqtn&_beX zAOt|8^C&<8KnDms1OQ0B`3?4zLuQFc8lNU+!X6?pZ6`}_10WQ%i0ETs{39M{r+rVt zI8r&KaWy`s6{K!(2K7zD?Htt{b>^w?-80>&Y>?2rM(w_w1dQ{IHPH7>*^@5n9zN1S z-VgJdsu3_|JgkPy?Jg@S(J9@Uy@bMk@GYw{=4mJD!Dw76y(nFXu)7wAV&&7QGjpxe zMDj6HH$Gwv$9TzjhDmVh^)}Kh0Rri8wrt+!gok88X91uUD1ZP!Fu_Xzp7||yt&+(C z#$bbq+<-mJo@Ss>3yX-zAKB_WkF51tm+{TlccW0^; zopOb`jx!XdXNe=l)rzMxT~Zl9Ie&@TTVcQAfNuL3bHKx0*6&t^vZ(1QxlA$E=aFBd z3PyOA=Q#0N(ihd(V6jcL2f-FmnPB{s;mA000000M0{Ia87J4R}%Qy XfG+?r+!9a#U;+;`01|XOAOHdYynO-T literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a203d0cdf13ea5106cdb8c98b52301f0e3719bac GIT binary patch literal 1053651 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%0A0_A2BB?T)i~ z4N2W5nUMXhn#m5ZjZ%8r@Zo^*hAJ$sW*4*wd7e z-^TSy`GRU+6j(e{#3C)$=TSoCvXhQ~i^5G`ofJ5H1Z3HM)UsT1Coa+z;Y7uEc0L$+P?b`&$h#6;Yn%gIcBF+k7a&xe@o$Bh~szTL@SG zT)$gE=cK;ka@n3^lyUynKCW8B-PT8qelkZD(p2E)OHfV`|IC)anf? zXHgh~dn(MW&9GyMCugeU#Cy||Bqd}VgD9hSYV51>!eh1?d3T5-#)3}6ZJUinQXnP^ z;*${1{1F^$7KNfuI$E)L=5yDpAh4BNO+c5$1xW7x#OG3owTv)i_)h!cjohtgw6V

Jbq-814NPd$TYkZAM*DuEbPgkd!5Mv&pQs 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%0A$ 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

F^h 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�L$2-vNN2qd&Na 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%0D@5 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%0Ar{$@$bdoE3f zl+)Mctbu8+cf2|ZjG4pJdASDk3rIyAtP$Vil5r_2J=$ajGZKuZ(6c+qweFga?LnP& z$B9ce!%KXsjXq5SRfiPXswS-iY((b=Cco(GpJg%HS)DkqdeAqujLe#wR1zIZXdXP{ z{tH_2aQ2**7FUgJ)s3-;>kvl}STp0|mAA_vm-_%l7@!GoQnEZj3wN(80xE$qG2{SZ zIMDDIsc1tgV(E1RH4YJVhAu$OSF2jgifS7B-^nsAh0uNyKAFA00E3gW={GK;j+f~h zohq|x14})MW>4@JUmZX(s#ELWl|&eClZa?zxfeI^w{P$BU}>beL!G4Rep3Q0XhY|# zxX-5XZ-SNl@9g(p$H;TR8^d%KYV;`5n>Y_aoKvj|F~_3CwYLL;Zix|MBKN2Gc{jQ^juMuW*km>*!flJ3%IgUW|mpYG8n(BlD(= zYaIL2*Z$588bvRM9b$(#z4=TEXV9>co`${%P2`k7B7-~6>N(aHVi(&XpuZ-pXx}8J%at2o*u<+f=PXCZey2HqS=mzo{*yq2fLiL}*XZ zFZeeaaI|A3?OjRz)Z6fr<&iM{;EgKuw)kJtQnzdIf=Lz8g5bD!>Ia zdi`-EPy%YiqM4s6@-AJS@SL0+)PksAdKJAS&*Ey};Xakz&^z;~{8{gTcy=q3YYClv zekUcJ%~sr=N{K9hX!$%3c3!|GFk!pb^G}j!x71<{nI&QBJUn2ymOZw%1&_z{D#Qo< z2$?==`s+b@XX*7fe2!fCvwA{W`$X9{i$w*gsz#xQ>Y{}mFMjgCOEcAfXwK!9Sp-=Z z9Usy4Z)(`b$B#j76y_&2ic<4cA7>hWa6R18Pl|eS%{#6@R=(Vqn!n)X_uW$mKnCW> z-*d;?76M%O1oGjbbt%9pej*EB^N1_ObjuI1Zh4YOKs@ncY-7 z2q=(=jyXe)u=tLTJ3$7Qzr*VJkHf9v#IOz>-5mCkOJz9fJ6>I0PLu7>h~29nGp1bc z8WiO5fOo6hUK4?w{!!hGwh^=aI}NAkma6QANzyC+1_`9i7#!|pW&6^cChdHbS5!S- zH!Ul?HZGc4CHp;M^{4EU@Olj=Bd$d*Z}RFF+fef(Zm?`MYCk7|d`lPn!FWqnQ`AA@ z4IU0gubhAd5BZBo`LAS86*b{r+#st5zw7dilQAoUbQ}twfKkI+qFZQi#`)^;o%Ru2 z^`^CM5Ohh6b9B)zbsa008rLxbYPmm=%ERRw~S9bM$Y0wsrahI#{GgU}6$>h<`*GDSby=9`8*n?2Yz8fw^8tTIp z^;e{gUf#2x;_5Dd#V9R@4JRhisFtJp!mXO!JUJxBWdF~6fNCq36aD{VKFHf$goW8; zT%)x&-uAMS6^e8$<9x{7((ihXPIjHt$77YB`Rek-MQUO+U^;GZ_nlOJ@z7=OISFHx z#^;?KyE;~DK58rpLf2Vm6az0E_wR4dR9h@t$)d*z2RLDaG_rYAX1k%auZNovLX;45 zJi)jrGgyF$L<3rvz=5k~dK zvFg1cs&u-v(42YX^B;@Ie*C`pjq+UIHhg;~EMiC1aF)SLYc}a-XDSlqQ7+o;qZhq8YUzO!c{RRD4xm>%0;ACrnaQ=0 zK)ht{r19Q3{cHDA6-qWhSpg�IeT~@r?-xXXpbi!xq0(h5wcR4zyG}?;yV7-{-lT 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�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 z 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

^$ywdz$5hK=ZG`Afn0K9XJjIQXceAoppq9T@ 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 OVI?aqAtfUr_5T3)u@DIW literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..33d5b340fa5a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1213 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_decision.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; +import 'package:webview_flutter_wkwebview_example/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. + const bool _skipDueToIssue86757 = false; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl('https://www.google.com/'); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }, skip: _skipDueToIssue86757); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: false, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +

+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com/"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: 'https://flutter.dev', + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller + .evaluateJavascript('window.open("https://www.google.com/")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion('https://www.google.com/')); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + }, + skip: _skipDueToIssue86757, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return await controller.evaluateJavascript(js); + } + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile new file mode 100644 index 000000000000..66509fcae284 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + inherit! :search_paths + + # Matches test_spec dependency. + pod 'OCMock', '3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..62428d041adf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F79266057800028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; + F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F71266057800028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, + 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, + F7151F75266057800028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + C6FFB52F5C2B8A41A7E39DE2 /* Pods */, + B6736FC417BDCCDA377E779D /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */, + F7151F74266057800028CB91 /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + B6736FC417BDCCDA377E779D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F75266057800028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F76266057800028CB91 /* FLTWebViewUITests.m */, + F7151F78266057800028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 68BDCAE523C3F7CB00D9C032 /* Sources */, + 68BDCAE623C3F7CB00D9C032 /* Frameworks */, + 68BDCAE723C3F7CB00D9C032 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = webview_flutter_exampleTests; + productReference = 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F73266057800028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F70266057800028CB91 /* Sources */, + F7151F71266057800028CB91 /* Frameworks */, + F7151F72266057800028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F7A266057800028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F74266057800028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 68BDCAE823C3F7CB00D9C032 = { + ProvisioningStyle = Automatic; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F73266057800028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */, + F7151F73266057800028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 68BDCAE723C3F7CB00D9C032 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F72266057800028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 68BDCAE523C3F7CB00D9C032 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F70266057800028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */; + }; + F7151F7A266057800028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F79266057800028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 68BDCAF023C3F7CB00D9C032 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 68BDCAF123C3F7CB00D9C032 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + F7151F7C266057800028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F7151F7D266057800028CB91 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 68BDCAF023C3F7CB00D9C032 /* Debug */, + 68BDCAF123C3F7CB00D9C032 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F7C266057800028CB91 /* Debug */, + F7151F7D266057800028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..d7453a8ce862 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3d43d11e66f4de3da27ed045ca4fe38ad8b48094 GIT binary patch literal 11112 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 P-1+_A+?hGkL;m(J7kc&K literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a810c5a172c0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + webview_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m new file mode 100644 index 000000000000..eb6d1543ec07 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTWKNavigationDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; +@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; + +@end + +@implementation FLTWKNavigationDelegateTests + +- (void)setUp { + self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); + self.navigationDelegate = + [[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel]; +} + +- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { + if (@available(iOS 9.0, *)) { + // `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above. + WKWebView *webview = OCMClassMock(WKWebView.class); + [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; + OCMVerify([self.mockMethodChannel + invokeMethod:@"onWebResourceError" + arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { + XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated"); + return true; + }]]); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m new file mode 100644 index 000000000000..631c4a105063 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FLTWebViewTests : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; + +@end + +@implementation FLTWebViewTests + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); +} + +- (void)testCanInitFLTWebViewController { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(controller); +} + +- (void)testCanInitFLTWebViewFactory { + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(factory); +} + +- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { + if (@available(iOS 11, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, + UIScrollViewContentInsetAdjustmentNever); + } +} + +- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { + if (@available(iOS 13, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); + } +} + +- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { + FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; + webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); + XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 200); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); + + if (@available(iOS 11, *)) { + // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. + UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); + UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); + OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 100); + XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m new file mode 100644 index 000000000000..d193be745972 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import XCTest; +@import os.log; + +@interface FLTWebViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation FLTWebViewUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testUserAgent { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* userAgent = app.buttons[@"Show user agent"]; + if (![userAgent waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Show user agent"); + } + NSPredicate* userAgentPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; + XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCTAssertFalse(userAgentPopUp.exists); + [userAgent tap]; + if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find user agent pop up"); + } +} + +- (void)testCache { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* clearCache = app.buttons[@"Clear cache"]; + if (![clearCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Clear cache"); + } + [clearCache tap]; + + [menu tap]; + + XCUIElement* listCache = app.buttons[@"List cache"]; + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find empty cache pop up"); + } + + [menu tap]; + XCUIElement* addCache = app.buttons[@"Add to cache"]; + if (![addCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Add to cache"); + } + [addCache tap]; + [menu tap]; + + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* cachePopup = + app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" + @"localStorage\":\"dummy_entry\"}}"]; + if (![cachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find cache pop up"); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart new file mode 100644 index 000000000000..a953e062ded5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; +import 'web_view.dart'; + +void main() { + runApp(MaterialApp(home: _WebViewExample())); +} + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

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

+
+ + +'''; + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + _NavigationControls(_controller.future), + _SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (context) { + return WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +Set _createJavascriptChannels(BuildContext context) { + return { + JavascriptChannel( + name: 'Snackbar', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message.message))); + }), + }; +} + +enum _MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class _SampleMenu extends StatelessWidget { + _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case _MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case _MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case _MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case _MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case _MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case _MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem<_MenuOptions>( + value: _MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Snackbar JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Snackbar.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await WebView.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class _NavigationControls extends StatelessWidget { + const _NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + if (controller == null) return Container(); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart new file mode 100644 index 000000000000..c1ff8dc5a690 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart new file mode 100644 index 000000000000..ddb8e9b0f14f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -0,0 +1,592 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + /// The WebView platform that's used by this WebView. + static final WebViewPlatform platform = CupertinoWebView(); + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller._updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController._( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + userAgent: widget.userAgent, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels( + _javascriptChannelRegistry.channels.values.toSet()); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); + } + + Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; + } + +// Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._webView); + + final WebView _webView; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_webView.onPageStarted != null) { + _webView.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_webView.onPageFinished != null) { + _webView.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_webView.onProgress != null) { + _webView.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_webView.onWebResourceError != null) { + _webView.onWebResourceError!(error); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml new file mode 100644 index 000000000000..229da5e337a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_wkwebview_example +description: Demonstrates how to use the webview_flutter_wkwebview plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter_wkwebview: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h new file mode 100644 index 000000000000..8fe331875250 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTCookieManager : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m new file mode 100644 index 000000000000..f4783ffb4123 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTCookieManager.h" + +@implementation FLTCookieManager { +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTCookieManager *instance = [[FLTCookieManager alloc] init]; + + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" + binaryMessenger:[registrar messenger]]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"clearCookies"]) { + [self clearCookies:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)clearCookies:(FlutterResult)result { + if (@available(iOS 9.0, *)) { + NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + + void (^deleteAndNotify)(NSArray *) = + ^(NSArray *cookies) { + BOOL hasCookies = cookies.count > 0; + [dataStore removeDataOfTypes:websiteDataTypes + forDataRecords:cookies + completionHandler:^{ + result(@(hasCookies)); + }]; + }; + + [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; + } else { + // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. + NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9."); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h new file mode 100644 index 000000000000..31edadc8cc05 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKNavigationDelegate : NSObject + +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel; + +/** + * Whether to delegate navigation decisions over the method channel. + */ +@property(nonatomic, assign) BOOL hasDartNavigationDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m new file mode 100644 index 000000000000..8b7ee7d0cfb7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m @@ -0,0 +1,116 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKNavigationDelegate.h" + +@implementation FLTWKNavigationDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + } + return self; +} + +#pragma mark - WKNavigationDelegate conformance + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + [_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}]; +} + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + if (!self.hasDartNavigationDelegate) { + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + NSDictionary *arguments = @{ + @"url" : navigationAction.request.URL.absoluteString, + @"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame) + }; + [_methodChannel invokeMethod:@"navigationRequest" + arguments:arguments + result:^(id _Nullable result) { + if ([result isKindOfClass:[FlutterError class]]) { + NSLog(@"navigationRequest has unexpectedly completed with an error, " + @"allowing navigation."); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + if (result == FlutterMethodNotImplemented) { + NSLog(@"navigationRequest was unexepectedly not implemented: %@, " + @"allowing navigation.", + result); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + if (![result isKindOfClass:[NSNumber class]]) { + NSLog(@"navigationRequest unexpectedly returned a non boolean value: " + @"%@, allowing navigation.", + result); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + NSNumber *typedResult = result; + decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow + : WKNavigationActionPolicyCancel); + }]; +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + [_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}]; +} + ++ (id)errorCodeToString:(NSUInteger)code { + switch (code) { + case WKErrorUnknown: + return @"unknown"; + case WKErrorWebContentProcessTerminated: + return @"webContentProcessTerminated"; + case WKErrorWebViewInvalidated: + return @"webViewInvalidated"; + case WKErrorJavaScriptExceptionOccurred: + return @"javaScriptExceptionOccurred"; + case WKErrorJavaScriptResultTypeIsUnsupported: + return @"javaScriptResultTypeIsUnsupported"; + } + + return [NSNull null]; +} + +- (void)onWebResourceError:(NSError *)error { + [_methodChannel invokeMethod:@"onWebResourceError" + arguments:@{ + @"errorCode" : @(error.code), + @"domain" : error.domain, + @"description" : error.description, + @"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code], + }]; +} + +- (void)webView:(WKWebView *)webView + didFailNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self onWebResourceError:error]; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self onWebResourceError:error]; +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { + NSError *contentProcessTerminatedError = + [[NSError alloc] initWithDomain:WKErrorDomain + code:WKErrorWebContentProcessTerminated + userInfo:nil]; + [self onWebResourceError:contentProcessTerminatedError]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h new file mode 100644 index 000000000000..96af4ef6c578 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKProgressionDelegate : NSObject + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; + +- (void)stopObservingProgress:(WKWebView *)webView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m new file mode 100644 index 000000000000..8e7af4649aa0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKProgressionDelegate.h" + +NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; + +@implementation FLTWKProgressionDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:FLTWKEstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + } + return self; +} + +- (void)stopObservingProgress:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { + NSNumber *newValue = + change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 + int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 + [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h new file mode 100644 index 000000000000..2a80c7d886f2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +@interface FLTWebViewFlutterPlugin : NSObject +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m new file mode 100644 index 000000000000..9f01416acc6a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWebViewFlutterPlugin.h" +#import "FLTCookieManager.h" +#import "FlutterWebView.h" + +@implementation FLTWebViewFlutterPlugin + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FLTWebViewFactory* webviewFactory = + [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; + [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; + [FLTCookieManager registerWithRegistrar:registrar]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h new file mode 100644 index 000000000000..6e795f7d1528 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWebViewController : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; + +- (UIView*)view; +@end + +@interface FLTWebViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject*)messenger; +@end + +/** + * The WkWebView used for the plugin. + * + * This class overrides some methods in `WKWebView` to serve the needs for the plugin. + */ +@interface FLTWKWebView : WKWebView +@end + +NS_ASSUME_NONNULL_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 new file mode 100644 index 000000000000..c6d926d3cfc2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -0,0 +1,491 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FlutterWebView.h" +#import "FLTWKNavigationDelegate.h" +#import "FLTWKProgressionDelegate.h" +#import "JavaScriptChannelHandler.h" + +@implementation FLTWebViewFactory { + NSObject* _messenger; +} + +- (instancetype)initWithMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; + return webviewController; +} + +@end + +@implementation FLTWKWebView + +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + self.scrollView.contentInset = UIEdgeInsetsZero; + // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of + // webview's contentInsets. + // 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); + } +} + +@end + +@implementation FLTWebViewController { + FLTWKWebView* _webView; + int64_t _viewId; + FlutterMethodChannel* _channel; + NSString* _currentUrl; + // The set of registered JavaScript channel names. + NSMutableSet* _javaScriptChannelNames; + FLTWKNavigationDelegate* _navigationDelegate; + FLTWKProgressionDelegate* _progressionDelegate; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger { + if (self = [super init]) { + _viewId = viewId; + + NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; + _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; + _javaScriptChannelNames = [[NSMutableSet alloc] init]; + + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { + NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"]; + [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; + [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; + } + + NSDictionary* settings = args[@"settings"]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + [self applyConfigurationSettings:settings toConfiguration:configuration]; + configuration.userContentController = userContentController; + [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] + inConfiguration:configuration]; + + _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; + _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; + _webView.UIDelegate = self; + _webView.navigationDelegate = _navigationDelegate; + __weak __typeof__(self) weakSelf = self; + [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [weakSelf onMethodCall:call result:result]; + }]; + + if (@available(iOS 11.0, *)) { + _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + if (@available(iOS 13.0, *)) { + _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; + } + } + + [self applySettings:settings]; + // TODO(amirh): return an error if apply settings failed once it's possible to do so. + // https://github.com/flutter/flutter/issues/36228 + + NSString* initialUrl = args[@"initialUrl"]; + if ([initialUrl isKindOfClass:[NSString class]]) { + [self loadUrl:initialUrl]; + } + } + return self; +} + +- (void)dealloc { + if (_progressionDelegate != nil) { + [_progressionDelegate stopObservingProgress:_webView]; + } +} + +- (UIView*)view { + return _webView; +} + +- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"updateSettings"]) { + [self onUpdateSettings:call result:result]; + } else if ([[call method] isEqualToString:@"loadUrl"]) { + [self onLoadUrl:call result:result]; + } else if ([[call method] isEqualToString:@"canGoBack"]) { + [self onCanGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"canGoForward"]) { + [self onCanGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"goBack"]) { + [self onGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"goForward"]) { + [self onGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"reload"]) { + [self onReload:call result:result]; + } else if ([[call method] isEqualToString:@"currentUrl"]) { + [self onCurrentUrl:call result:result]; + } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { + [self onEvaluateJavaScript:call result:result]; + } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { + [self onAddJavaScriptChannels:call result:result]; + } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { + [self onRemoveJavaScriptChannels:call result:result]; + } else if ([[call method] isEqualToString:@"clearCache"]) { + [self clearCache:result]; + } else if ([[call method] isEqualToString:@"getTitle"]) { + [self onGetTitle:result]; + } else if ([[call method] isEqualToString:@"scrollTo"]) { + [self onScrollTo:call result:result]; + } else if ([[call method] isEqualToString:@"scrollBy"]) { + [self onScrollBy:call result:result]; + } else if ([[call method] isEqualToString:@"getScrollX"]) { + [self getScrollX:call result:result]; + } else if ([[call method] isEqualToString:@"getScrollY"]) { + [self getScrollY:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* error = [self applySettings:[call arguments]]; + if (error == nil) { + result(nil); + return; + } + result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); +} + +- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { + if (![self loadRequest:[call arguments]]) { + result([FlutterError + errorWithCode:@"loadUrl_failed" + message:@"Failed parsing the URL" + details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); + } else { + result(nil); + } +} + +- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoBack = [_webView canGoBack]; + result(@(canGoBack)); +} + +- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoForward = [_webView canGoForward]; + result(@(canGoForward)); +} + +- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goBack]; + result(nil); +} + +- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goForward]; + result(nil); +} + +- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView reload]; + result(nil); +} + +- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { + _currentUrl = [[_webView URL] absoluteString]; + result(_currentUrl); +} + +- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* jsString = [call arguments]; + if (!jsString) { + result([FlutterError errorWithCode:@"evaluateJavaScript_failed" + message:@"JavaScript String cannot be null" + details:nil]); + return; + } + [_webView evaluateJavaScript:jsString + completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { + if (error) { + result([FlutterError + errorWithCode:@"evaluateJavaScript_failed" + message:@"Failed evaluating JavaScript" + details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", + jsString, error]]); + } else { + result([NSString stringWithFormat:@"%@", evaluateResult]); + } + }]; +} + +- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { + NSArray* channelNames = [call arguments]; + NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; + [_javaScriptChannelNames addObjectsFromArray:channelNames]; + [self registerJavaScriptChannels:channelNamesSet + controller:_webView.configuration.userContentController]; + result(nil); +} + +- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { + // WkWebView does not support removing a single user script, so instead we remove all + // user scripts, all message handlers. And re-register channels that shouldn't be removed. + [_webView.configuration.userContentController removeAllUserScripts]; + for (NSString* channelName in _javaScriptChannelNames) { + [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; + } + + NSArray* channelNamesToRemove = [call arguments]; + for (NSString* channelName in channelNamesToRemove) { + [_javaScriptChannelNames removeObject:channelName]; + } + + [self registerJavaScriptChannels:_javaScriptChannelNames + controller:_webView.configuration.userContentController]; + result(nil); +} + +- (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."); + } +} + +- (void)onGetTitle:(FlutterResult)result { + NSString* title = _webView.title; + result(title); +} + +- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary* arguments = [call arguments]; + int x = [arguments[@"x"] intValue]; + int y = [arguments[@"y"] intValue]; + + _webView.scrollView.contentOffset = CGPointMake(x, y); + result(nil); +} + +- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { + CGPoint contentOffset = _webView.scrollView.contentOffset; + + NSDictionary* arguments = [call arguments]; + int x = [arguments[@"x"] intValue] + contentOffset.x; + int y = [arguments[@"y"] intValue] + contentOffset.y; + + _webView.scrollView.contentOffset = CGPointMake(x, y); + result(nil); +} + +- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { + int offsetX = _webView.scrollView.contentOffset.x; + result(@(offsetX)); +} + +- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { + int offsetY = _webView.scrollView.contentOffset.y; + result(@(offsetY)); +} + +// Returns nil when successful, or an error message when one or more keys are unknown. +- (NSString*)applySettings:(NSDictionary*)settings { + NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; + for (NSString* key in settings) { + if ([key isEqualToString:@"jsMode"]) { + NSNumber* mode = settings[key]; + [self updateJsMode:mode]; + } else if ([key isEqualToString:@"hasNavigationDelegate"]) { + NSNumber* hasDartNavigationDelegate = settings[key]; + _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"hasProgressTracking"]) { + NSNumber* hasProgressTrackingValue = settings[key]; + bool hasProgressTracking = [hasProgressTrackingValue boolValue]; + if (hasProgressTracking) { + _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView + channel:_channel]; + } + } else if ([key isEqualToString:@"debuggingEnabled"]) { + // no-op debugging is always enabled on iOS. + } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { + NSNumber* allowsBackForwardNavigationGestures = settings[key]; + _webView.allowsBackForwardNavigationGestures = + [allowsBackForwardNavigationGestures boolValue]; + } else if ([key isEqualToString:@"userAgent"]) { + NSString* userAgent = settings[key]; + [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; + } else { + [unknownKeys addObject:key]; + } + } + if ([unknownKeys count] == 0) { + return nil; + } + return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", + [unknownKeys componentsJoinedByString:@", "]]; +} + +- (void)applyConfigurationSettings:(NSDictionary*)settings + toConfiguration:(WKWebViewConfiguration*)configuration { + NSAssert(configuration != _webView.configuration, + @"configuration needs to be updated before webView.configuration."); + for (NSString* key in settings) { + if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { + NSNumber* allowsInlineMediaPlayback = settings[key]; + configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; + } + } +} + +- (void)updateJsMode:(NSNumber*)mode { + WKPreferences* preferences = [[_webView configuration] preferences]; + switch ([mode integerValue]) { + case 0: // disabled + [preferences setJavaScriptEnabled:NO]; + break; + case 1: // unrestricted + [preferences setJavaScriptEnabled:YES]; + break; + default: + NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); + } +} + +- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy + inConfiguration:(WKWebViewConfiguration*)configuration { + switch ([policy integerValue]) { + case 0: // require_user_action_for_all_media_types + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = true; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = true; +#pragma clang diagnostic pop + } + break; + case 1: // always_allow + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = false; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = false; +#pragma clang diagnostic pop + } + break; + default: + NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); + } +} + +- (bool)loadRequest:(NSDictionary*)request { + if (!request) { + return false; + } + + NSString* url = request[@"url"]; + if ([url isKindOfClass:[NSString class]]) { + id headers = request[@"headers"]; + if ([headers isKindOfClass:[NSDictionary class]]) { + return [self loadUrl:url withHeaders:headers]; + } else { + return [self loadUrl:url]; + } + } + + return false; +} + +- (bool)loadUrl:(NSString*)url { + return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; +} + +- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { + NSURL* nsUrl = [NSURL URLWithString:url]; + if (!nsUrl) { + return false; + } + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; + [request setAllHTTPHeaderFields:headers]; + [_webView loadRequest:request]; + return true; +} + +- (void)registerJavaScriptChannels:(NSSet*)channelNames + controller:(WKUserContentController*)userContentController { + for (NSString* channelName in channelNames) { + FLTJavaScriptChannel* channel = + [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel + javaScriptChannelName:channelName]; + [userContentController addScriptMessageHandler:channel name:channelName]; + NSString* wrapperSource = [NSString + stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; + WKUserScript* wrapperScript = + [[WKUserScript alloc] initWithSource:wrapperSource + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + [userContentController addUserScript:wrapperScript]; + } +} + +- (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."); + } +} + +#pragma mark WKUIDelegate + +- (WKWebView*)webView:(WKWebView*)webView + createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration + forNavigationAction:(WKNavigationAction*)navigationAction + windowFeatures:(WKWindowFeatures*)windowFeatures { + if (!navigationAction.targetFrame.isMainFrame) { + [webView loadRequest:navigationAction.request]; + } + + return nil; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h new file mode 100644 index 000000000000..a0a5ec657295 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTJavaScriptChannel : NSObject + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel + javaScriptChannelName:(NSString*)javaScriptChannelName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m new file mode 100644 index 000000000000..ec9a363a4b2e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "JavaScriptChannelHandler.h" + +@implementation FLTJavaScriptChannel { + FlutterMethodChannel* _methodChannel; + NSString* _javaScriptChannelName; +} + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel + javaScriptChannelName:(NSString*)javaScriptChannelName { + self = [super init]; + NSAssert(methodChannel != nil, @"methodChannel must not be null."); + NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null."); + if (self) { + _methodChannel = methodChannel; + _javaScriptChannelName = javaScriptChannelName; + } + return self; +} + +- (void)userContentController:(WKUserContentController*)userContentController + didReceiveScriptMessage:(WKScriptMessage*)message { + NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel."); + NSAssert(_javaScriptChannelName != nil, + @"Can't send a message to an unitialized JavaScript channel."); + NSDictionary* arguments = @{ + @"channel" : _javaScriptChannelName, + @"message" : [NSString stringWithFormat:@"%@", message.body] + }; + [_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec new file mode 100644 index 000000000000..37905f147489 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'webview_flutter_wkwebview' + s.version = '0.0.1' + s.summary = 'A WebView Plugin for Flutter.' + s.description = <<-DESC +A Flutter plugin that provides a WebView widget. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview' } + s.documentation_url = 'https://pub.dev/packages/webview_flutter' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart new file mode 100644 index 000000000000..05b79d0a72e4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an iOS webview. +/// +/// This is used as the default implementation for [WebView.platform] on iOS. It uses +/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class CupertinoWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart new file mode 100644 index 000000000000..bbec415dccd0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/webview_cupertino.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml new file mode 100644 index 000000000000..c6f6d6f94f07 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -0,0 +1,29 @@ +name: webview_flutter_wkwebview +description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.0.13 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + ios: + pluginClass: FLTWebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + webview_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 \ No newline at end of file From 5ec9d0b37bfd5a656df005518cfd3d869b70f3ea Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 21 Sep 2021 15:03:58 -0400 Subject: [PATCH 060/259] [flutter_plugin_tools] Fix federated safety check (#4368) The new safety check doesn't allow simple platform-interface-only changes because it doesn't actually check that a non-interface package is actually modified before failing it for a modified platform interface. This fixes that, and adds a test case covering it. --- .../src/federation_safety_check_command.dart | 7 ++++ .../federation_safety_check_command_test.dart | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index cb0da162e604..fd53d6cbaa67 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -135,6 +135,13 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { return PackageResult.success(); } + final List changedPackageFiles = + _changedDartFiles[package.directory.basename] ?? []; + if (changedPackageFiles.isEmpty) { + print('No Dart changes.'); + return PackageResult.success(); + } + // If the change would be flagged, but it appears to be a mass change // rather than a plugin-specific change, allow it with a warning. // diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index 4ae3ec5c76d0..e23485fbc8b7 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -125,6 +125,47 @@ void main() { ); }); + test('allows changes to just an interface package', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + createFakePlugin('foo', pluginGroupDir); + createFakePlugin('foo_ios', pluginGroupDir); + createFakePlugin('foo_android', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No Dart changes.'), + contains('Running for foo_android...'), + contains('No Dart changes.'), + contains('Running for foo_ios...'), + contains('No Dart changes.'), + contains('Running for foo_platform_interface...'), + contains('Ran for 3 package(s)'), + contains('Skipped 1 package(s)'), + ]), + ); + expect( + output, + isNot(contains([ + contains('No published changes for foo_platform_interface'), + ])), + ); + }); + test('allows changes to multiple non-interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); final Directory appFacing = createFakePlugin('foo', pluginGroupDir); From 42a17dc4de1ca8a87631095fb74fb669c7e522ed Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 21 Sep 2021 15:04:34 -0400 Subject: [PATCH 061/259] Require authors file (#4367) Adds a check to `publish-check` that there is an AUTHORS file present, since our license refers to "The Flutter Authors", so we want to have a file distributed with each package that says who the AUTHORS are (vs. just having a top-level repo AUTHORS file, which is not part of package distribution). Adds AUTHORS files to packages that have been created since the earlier one-time fix that added them, but didn't add a check to prevent future issues. Also updates the publish-check failure tests to include checks for specific output so that we know that they are failing for the reasons the test is expecting, bringing them up to current repo standards for failure tests. Fixes https://github.com/flutter/flutter/issues/89680 --- packages/camera/camera_web/AUTHORS | 66 ++++++++++++++ .../in_app_purchase_android/AUTHORS | 67 ++++++++++++++ .../in_app_purchase_ios/AUTHORS | 67 ++++++++++++++ .../AUTHORS | 67 ++++++++++++++ packages/quick_actions/quick_actions/AUTHORS | 67 ++++++++++++++ .../quick_actions_platform_interface/AUTHORS | 67 ++++++++++++++ script/tool/CHANGELOG.md | 1 + .../tool/lib/src/publish_check_command.dart | 21 ++++- script/tool/test/list_command_test.dart | 3 + .../tool/test/publish_check_command_test.dart | 89 +++++++++++++++++-- script/tool/test/util.dart | 7 ++ 11 files changed, 512 insertions(+), 10 deletions(-) create mode 100644 packages/camera/camera_web/AUTHORS create mode 100644 packages/in_app_purchase/in_app_purchase_android/AUTHORS create mode 100644 packages/in_app_purchase/in_app_purchase_ios/AUTHORS create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS create mode 100644 packages/quick_actions/quick_actions/AUTHORS create mode 100644 packages/quick_actions/quick_actions_platform_interface/AUTHORS diff --git a/packages/camera/camera_web/AUTHORS b/packages/camera/camera_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/in_app_purchase/in_app_purchase_android/AUTHORS b/packages/in_app_purchase/in_app_purchase_android/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/in_app_purchase/in_app_purchase_ios/AUTHORS b/packages/in_app_purchase/in_app_purchase_ios/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS b/packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/quick_actions/quick_actions/AUTHORS b/packages/quick_actions/quick_actions/AUTHORS new file mode 100644 index 000000000000..0ca697b6a756 --- /dev/null +++ b/packages/quick_actions/quick_actions/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Daniel Roek diff --git a/packages/quick_actions/quick_actions_platform_interface/AUTHORS b/packages/quick_actions/quick_actions_platform_interface/AUTHORS new file mode 100644 index 000000000000..0ca697b6a756 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Daniel Roek diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3be9173a505b..7e9cd3bec938 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,7 @@ - Added a new `federation-safety-check` command to help catch changes to federated packages that have been done in such a way that they will pass in CI, but fail once the change is landed and published. +- `publish-check` now validates that there is an `AUTHORS` file. ## 0.7.1 diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index ab9f5f147495..563e0904552a 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -77,10 +77,17 @@ class PublishCheckCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final _PublishCheckResult? result = await _passesPublishCheck(package); + _PublishCheckResult? result = await _passesPublishCheck(package); if (result == null) { return PackageResult.skip('Package is marked as unpublishable.'); } + if (!_passesAuthorsCheck(package)) { + _printImportantStatusMessage( + 'No AUTHORS file found. Packages must include an AUTHORS file.', + isError: true); + result = _PublishCheckResult.error; + } + if (result.index > _overallResult.index) { _overallResult = result; } @@ -189,7 +196,7 @@ class PublishCheckCommand extends PackageLoopingCommand { final String packageName = package.directory.basename; final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { - print('no pubspec'); + print('No valid pubspec found.'); return _PublishCheckResult.error; } else if (pubspec.publishTo == 'none') { return null; @@ -239,6 +246,16 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } + bool _passesAuthorsCheck(RepositoryPackage package) { + final List pathComponents = + package.directory.fileSystem.path.split(package.directory.path); + if (pathComponents.contains('third_party')) { + // Third-party packages aren't required to have an AUTHORS file. + return true; + } + return package.directory.childFile('AUTHORS').existsSync(); + } + void _printImportantStatusMessage(String message, {required bool isError}) { final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; if (getBoolArg(_machineFlag)) { diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index 488fc9bcb1e4..fcdf9fafdb63 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -99,13 +99,16 @@ void main() { examples, unorderedEquals([ '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/AUTHORS', '/packages/plugin1/CHANGELOG.md', '/packages/plugin1/example/pubspec.yaml', '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/AUTHORS', '/packages/plugin2/CHANGELOG.md', '/packages/plugin2/example/example1/pubspec.yaml', '/packages/plugin2/example/example2/pubspec.yaml', '/packages/plugin3/pubspec.yaml', + '/packages/plugin3/AUTHORS', '/packages/plugin3/CHANGELOG.md', ]), ); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index e1ab0e224e44..c5527af21736 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -69,12 +69,22 @@ void main() { createFakePlugin('plugin_tools_test_package_a', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) + MockProcess(exitCode: 1, stdout: 'Some error from pub') ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - () => runCapturingPrint(runner, ['publish-check']), - throwsA(isA()), + output, + containsAllInOrder([ + contains('Some error from pub'), + contains('Unable to publish plugin_tools_test_package_a'), + ]), ); }); @@ -82,8 +92,58 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - expect(() => runCapturingPrint(runner, ['publish-check']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No valid pubspec found.'), + ]), + ); + }); + + test('fails if AUTHORS is missing', () async { + final Directory package = createFakePackage('a_package', packagesDir); + package.childFile('AUTHORS').delete(); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'No AUTHORS file found. Packages must include an AUTHORS file.'), + ]), + ); + }); + + test('does not require AUTHORS for third-party', () async { + final Directory package = createFakePackage( + 'a_package', + packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages')); + package.childFile('AUTHORS').delete(); + + final List output = + await runCapturingPrint(runner, ['publish-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + ]), + ); }); test('pass on prerelease if --allow-pre-release flag is on', () async { @@ -116,8 +176,21 @@ void main() { process, ]; - expect(runCapturingPrint(runner, ['publish-check']), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK'), + contains('Unable to publish d'), + ]), + ); }); test('Success message on stderr is not printed as an error', () async { @@ -324,7 +397,7 @@ void main() { // aren't controlled by this package, so asserting its exact format would // make the test fragile to irrelevant changes in those details. expect(output.first, contains(r''' - "no pubspec", + "No valid pubspec found.", "\n============================================================\n|| Running for no_publish_b\n============================================================\n", "url https://pub.dev/packages/no_publish_b.json", "no_publish_b.json", diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index e053100172c6..9abb34bef35a 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -126,6 +126,7 @@ Directory createFakePackage( ## $version * Some changes. '''); + createFakeAuthors(packageDirectory); if (examples.length == 1) { final Directory exampleDir = packageDirectory.childDirectory(examples.first) @@ -208,6 +209,12 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being parent.childFile('pubspec.yaml').writeAsStringSync(yaml); } +void createFakeAuthors(Directory parent) { + final File authorsFile = parent.childFile('AUTHORS'); + authorsFile.createSync(); + authorsFile.writeAsStringSync('Google Inc.'); +} + String _pluginPlatformSection( String platform, PlatformDetails support, String packageName) { String entry = ''; From ed2e7a78aee8e461be7d1b1aa1212770bf3d00b1 Mon Sep 17 00:00:00 2001 From: Rafael_Ferraz Date: Wed, 22 Sep 2021 02:33:05 -0300 Subject: [PATCH 062/259] [video_player] VTT Support (#2878) --- .../video_player/video_player/CHANGELOG.md | 4 + .../example/assets/bumble_bee_captions.vtt | 7 + .../video_player/example/lib/main.dart | 5 +- .../video_player/example/pubspec.yaml | 7 +- .../lib/src/closed_caption_file.dart | 4 + .../video_player/lib/src/sub_rip.dart | 20 +- .../video_player/lib/src/web_vtt.dart | 211 ++++++++++++++ .../video_player/video_player/pubspec.yaml | 3 +- .../video_player/test/web_vtt_test.dart | 261 ++++++++++++++++++ 9 files changed, 507 insertions(+), 15 deletions(-) create mode 100644 packages/video_player/video_player/example/assets/bumble_bee_captions.vtt create mode 100644 packages/video_player/video_player/lib/src/web_vtt.dart create mode 100644 packages/video_player/video_player/test/web_vtt_test.dart diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index de60af49b95d..539a5520e5be 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.5 + +* Support to closed caption WebVTT format added. + ## 2.2.4 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt new file mode 100644 index 000000000000..1dca2c58695e --- /dev/null +++ b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt @@ -0,0 +1,7 @@ +WEBVTT + +00:00:00.200 --> 00:00:01.750 +[ Birds chirping ] + +00:00:02.300 --> 00:00:05.000 +[ Buzzing ] diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index eef23197ef50..f035720396dd 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -210,8 +210,9 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) - .loadString('assets/bumble_bee_captions.srt'); - return SubRipCaptionFile(fileContents); + .loadString('assets/bumble_bee_captions.vtt'); + return WebVTTCaptionFile( + fileContents); // For vtt files, use WebVTTCaptionFile } @override diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 63f179a06211..0539f3c6f56c 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -30,6 +30,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/flutter-mark-square-64.png - - assets/Butterfly-209.mp4 - - assets/bumble_bee_captions.srt + - assets/flutter-mark-square-64.png + - assets/Butterfly-209.mp4 + - assets/bumble_bee_captions.srt + - assets/bumble_bee_captions.vtt diff --git a/packages/video_player/video_player/lib/src/closed_caption_file.dart b/packages/video_player/video_player/lib/src/closed_caption_file.dart index 3c7d69b89598..e410e2652ad3 100644 --- a/packages/video_player/video_player/lib/src/closed_caption_file.dart +++ b/packages/video_player/video_player/lib/src/closed_caption_file.dart @@ -5,6 +5,9 @@ import 'sub_rip.dart'; export 'sub_rip.dart' show SubRipCaptionFile; +import 'web_vtt.dart'; +export 'web_vtt.dart' show WebVTTCaptionFile; + /// A structured representation of a parsed closed caption file. /// /// A closed caption file includes a list of captions, each with a start and end @@ -15,6 +18,7 @@ export 'sub_rip.dart' show SubRipCaptionFile; /// /// See: /// * [SubRipCaptionFile]. +/// * [WebVTTCaptionFile]. abstract class ClosedCaptionFile { /// The full list of captions from a given file. /// diff --git a/packages/video_player/video_player/lib/src/sub_rip.dart b/packages/video_player/video_player/lib/src/sub_rip.dart index 73cd8266c2e9..5d6863f72bb8 100644 --- a/packages/video_player/video_player/lib/src/sub_rip.dart +++ b/packages/video_player/video_player/lib/src/sub_rip.dart @@ -16,6 +16,8 @@ class SubRipCaptionFile extends ClosedCaptionFile { : _captions = _parseCaptionsFromSubRipString(fileContents); /// The entire body of the SubRip file. + // TODO(cyanglaz): Remove this public member as it doesn't seem need to exist. + // https://github.com/flutter/flutter/issues/90471 final String fileContents; @override @@ -30,15 +32,15 @@ List _parseCaptionsFromSubRipString(String file) { if (captionLines.length < 3) break; final int captionNumber = int.parse(captionLines[0]); - final _StartAndEnd startAndEnd = - _StartAndEnd.fromSubRipString(captionLines[1]); + final _CaptionRange captionRange = + _CaptionRange.fromSubRipString(captionLines[1]); final String text = captionLines.sublist(2).join('\n'); final Caption newCaption = Caption( number: captionNumber, - start: startAndEnd.start, - end: startAndEnd.end, + start: captionRange.start, + end: captionRange.end, text: text, ); if (newCaption.start != newCaption.end) { @@ -49,21 +51,21 @@ List _parseCaptionsFromSubRipString(String file) { return captions; } -class _StartAndEnd { +class _CaptionRange { final Duration start; final Duration end; - _StartAndEnd(this.start, this.end); + _CaptionRange(this.start, this.end); // Assumes format from an SubRip file. // For example: // 00:01:54,724 --> 00:01:56,760 - static _StartAndEnd fromSubRipString(String line) { + static _CaptionRange fromSubRipString(String line) { final RegExp format = RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); if (!format.hasMatch(line)) { - return _StartAndEnd(Duration.zero, Duration.zero); + return _CaptionRange(Duration.zero, Duration.zero); } final List times = line.split(_subRipArrow); @@ -71,7 +73,7 @@ class _StartAndEnd { final Duration start = _parseSubRipTimestamp(times[0]); final Duration end = _parseSubRipTimestamp(times[1]); - return _StartAndEnd(start, end); + return _CaptionRange(start, end); } } diff --git a/packages/video_player/video_player/lib/src/web_vtt.dart b/packages/video_player/video_player/lib/src/web_vtt.dart new file mode 100644 index 000000000000..6c4527d34d67 --- /dev/null +++ b/packages/video_player/video_player/lib/src/web_vtt.dart @@ -0,0 +1,211 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:html/dom.dart'; + +import 'closed_caption_file.dart'; +import 'package:html/parser.dart' as html_parser; + +/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format. +/// See: https://en.wikipedia.org/wiki/WebVTT +class WebVTTCaptionFile extends ClosedCaptionFile { + /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in + /// the WebVTT file format. + /// * See: https://en.wikipedia.org/wiki/WebVTT + WebVTTCaptionFile(String fileContents) + : _captions = _parseCaptionsFromWebVTTString(fileContents); + + @override + List get captions => _captions; + + final List _captions; +} + +List _parseCaptionsFromWebVTTString(String file) { + final List captions = []; + + // Ignore metadata + Set metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'}; + + int captionNumber = 1; + for (List captionLines in _readWebVTTFile(file)) { + // CaptionLines represent a complete caption. + // E.g + // [ + // [00:00.000 --> 01:24.000 align:center] + // ['Introduction'] + // ] + // If caption has just header or time, but no text, `captionLines.length` will be 1. + if (captionLines.length < 2) continue; + + // If caption has header equal metadata, ignore. + String metadaType = captionLines[0].split(' ')[0]; + if (metadata.contains(metadaType)) continue; + + // Caption has header + bool hasHeader = captionLines.length > 2; + if (hasHeader) { + final int? tryParseCaptionNumber = int.tryParse(captionLines[0]); + if (tryParseCaptionNumber != null) { + captionNumber = tryParseCaptionNumber; + } + } + + final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString( + hasHeader ? captionLines[1] : captionLines[0], + ); + + if (captionRange == null) { + continue; + } + + final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n'); + + // TODO(cyanglaz): Handle special syntax in VTT captions. + // https://github.com/flutter/flutter/issues/90007. + final String textWithoutFormat = _extractTextFromHtml(text); + + final Caption newCaption = Caption( + number: captionNumber, + start: captionRange.start, + end: captionRange.end, + text: textWithoutFormat, + ); + captions.add(newCaption); + captionNumber++; + } + + return captions; +} + +class _CaptionRange { + final Duration start; + final Duration end; + + _CaptionRange(this.start, this.end); + + // Assumes format from an VTT file. + // For example: + // 00:09.000 --> 00:11.000 + static _CaptionRange? fromWebVTTString(String line) { + final RegExp format = + RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); + + if (!format.hasMatch(line)) { + return null; + } + + final List times = line.split(_webVTTArrow); + + final Duration? start = _parseWebVTTTimestamp(times[0]); + final Duration? end = _parseWebVTTTimestamp(times[1]); + + if (start == null || end == null) { + return null; + } + + return _CaptionRange(start, end); + } +} + +String _extractTextFromHtml(String htmlString) { + final Document document = html_parser.parse(htmlString); + final Element? body = document.body; + if (body == null) { + return ''; + } + final Element? bodyElement = html_parser.parse(body.text).documentElement; + return bodyElement?.text ?? ''; +} + +// Parses a time stamp in an VTT file into a Duration. +// +// Returns `null` if `timestampString` is in an invalid format. +// +// For example: +// +// _parseWebVTTTimestamp('00:01:08.430') +// returns +// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430) +Duration? _parseWebVTTTimestamp(String timestampString) { + if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) { + return null; + } + + final List dotSections = timestampString.split('.'); + final List timeComponents = dotSections[0].split(':'); + + // Validating and parsing the `timestampString`, invalid format will result this method + // to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid + // WebVTT timestamp format. + if (timeComponents.length > 3 || timeComponents.length < 2) { + return null; + } + int hours = 0; + if (timeComponents.length == 3) { + final String hourString = timeComponents.removeAt(0); + if (hourString.length < 2) { + return null; + } + hours = int.parse(hourString); + } + final int minutes = int.parse(timeComponents.removeAt(0)); + if (minutes < 0 || minutes > 59) { + return null; + } + final int seconds = int.parse(timeComponents.removeAt(0)); + if (seconds < 0 || seconds > 59) { + return null; + } + + List milisecondsStyles = dotSections[1].split(" "); + + // TODO(cyanglaz): Handle caption styles. + // https://github.com/flutter/flutter/issues/90009. + // ```dart + // if (milisecondsStyles.length > 1) { + // List styles = milisecondsStyles.sublist(1); + // } + // ``` + // For a better readable code style, style parsing should happen before + // calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134. + int milliseconds = int.parse(milisecondsStyles[0]); + + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); +} + +// Reads on VTT file and splits it into Lists of strings where each list is one +// caption. +List> _readWebVTTFile(String file) { + final List lines = LineSplitter.split(file).toList(); + + final List> captionStrings = >[]; + List currentCaption = []; + int lineIndex = 0; + for (final String line in lines) { + final bool isLineBlank = line.trim().isEmpty; + if (!isLineBlank) { + currentCaption.add(line); + } + + if (isLineBlank || lineIndex == lines.length - 1) { + captionStrings.add(currentCaption); + currentCaption = []; + } + + lineIndex += 1; + } + + return captionStrings; +} + +const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})'; +const String _webVTTArrow = r' --> '; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 926add50f43c..a6ee2d594656 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.4 +version: 2.2.5 environment: sdk: ">=2.14.0 <3.0.0" @@ -32,6 +32,7 @@ dependencies: # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 video_player_web: ^2.0.0 + html: ^0.15.0 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player/test/web_vtt_test.dart b/packages/video_player/video_player/test/web_vtt_test.dart new file mode 100644 index 000000000000..59fce98c5b71 --- /dev/null +++ b/packages/video_player/video_player/test/web_vtt_test.dart @@ -0,0 +1,261 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/src/closed_caption_file.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + group('Parse VTT file', () { + WebVTTCaptionFile parsedFile; + + test('with Metadata', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_metadata); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, Duration(seconds: 1)); + expect( + parsedFile.captions[0].end, Duration(seconds: 2, milliseconds: 500)); + expect(parsedFile.captions[0].text, 'We are in New York City'); + }); + + test('with Multiline', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_multiline); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, + Duration(seconds: 2, milliseconds: 800)); + expect( + parsedFile.captions[0].end, Duration(seconds: 3, milliseconds: 283)); + expect(parsedFile.captions[0].text, + "— It will perforate your stomach.\n— You could die."); + }); + + test('with styles tags', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_styles); + expect(parsedFile.captions.length, 3); + + expect(parsedFile.captions[0].start, + Duration(seconds: 5, milliseconds: 200)); + expect( + parsedFile.captions[0].end, Duration(seconds: 6, milliseconds: 000)); + expect(parsedFile.captions[0].text, + "You know I'm so excited my glasses are falling off here."); + }); + + test('with subtitling features', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_subtitling_features); + expect(parsedFile.captions.length, 3); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 4)); + expect(parsedFile.captions.last.end, Duration(seconds: 5)); + expect(parsedFile.captions.last.text, "Transcrit par Célestes™"); + }); + + test('with [hours]:[minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 1)); + expect(parsedFile.captions.last.end, Duration(seconds: 2)); + expect(parsedFile.captions.last.text, "This is a test."); + }); + + test('with [minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_without_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 3)); + expect(parsedFile.captions.last.end, Duration(seconds: 4)); + expect(parsedFile.captions.last.text, "This is a test."); + }); + + test('with invalid seconds format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_seconds); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid minutes format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_minutes); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid hours format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_hours); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid component length returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_time_component_too_long); + expect(parsedFile.captions, isEmpty); + + parsedFile = WebVTTCaptionFile(_time_component_too_short); + expect(parsedFile.captions, isEmpty); + }); + }); + + test('Parses VTT file with malformed input.', () { + final ClosedCaptionFile parsedFile = WebVTTCaptionFile(_malformedVTT); + + expect(parsedFile.captions.length, 1); + + final Caption firstCaption = parsedFile.captions.single; + expect(firstCaption.number, 1); + expect(firstCaption.start, Duration(seconds: 13)); + expect(firstCaption.end, Duration(seconds: 16, milliseconds: 0)); + expect(firstCaption.text, 'Valid'); + }); +} + +/// See https://www.w3.org/TR/webvtt1/#introduction-comments +const String _valid_vtt_with_metadata = ''' +WEBVTT Kind: captions; Language: en + +REGION +id:bill +width:40% +lines:3 +regionanchor:100%,100% +viewportanchor:90%,90% +scroll:up + +NOTE +This file was written by Jill. I hope +you enjoy reading it. Some things to +bear in mind: +- I was lip-reading, so the cues may +not be 100% accurate +- I didn’t pay too close attention to +when the cues should start or end. + +1 +00:01.000 --> 00:02.500 +We are in New York City +'''; + +/// See https://www.w3.org/TR/webvtt1/#introduction-multiple-lines +const String _valid_vtt_with_multiline = ''' +WEBVTT + +2 +00:02.800 --> 00:03.283 +— It will perforate your stomach. +— You could die. + +'''; + +/// See https://www.w3.org/TR/webvtt1/#styling +const String _valid_vtt_with_styles = ''' +WEBVTT + +00:05.200 --> 00:06.000 align:start size:50% +You know I'm so excited my glasses are falling off here. + +00:00:06.050 --> 00:00:06.150 +I have a different time! + +00:06.200 --> 00:06.900 +This is yellow text on a blue background + +'''; + +//See https://www.w3.org/TR/webvtt1/#introduction-other-features +const String _valid_vtt_with_subtitling_features = ''' +WEBVTT + +test +00:00.000 --> 00:02.000 +This is a test. + +Slide 1 +00:00:00.000 --> 00:00:10.700 +Title Slide + +crédit de transcription +00:04.000 --> 00:05.000 +Transcrit par Célestes™ + +'''; + +/// With format [hours]:[minutes]:[seconds].[milliseconds] +const String _valid_vtt_with_hours = ''' +WEBVTT + +test +00:00:01.000 --> 00:00:02.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _invalid_seconds = ''' +WEBVTT + +60:00:000.000 --> 60:02:000.000 +This is a test. + +'''; + +/// Invalid minutes format. +const String _invalid_minutes = ''' +WEBVTT + +60:60:00.000 --> 60:70:00.000 +This is a test. + +'''; + +/// Invalid hours format. +const String _invalid_hours = ''' +WEBVTT + +5:00:00.000 --> 5:02:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_long = ''' +WEBVTT + +60:00:00:00.000 --> 60:02:00:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_short = ''' +WEBVTT + +60:00.000 --> 60:02.000 +This is a test. + +'''; + +/// With format [minutes]:[seconds].[milliseconds] +const String _valid_vtt_without_hours = ''' +WEBVTT + +00:03.000 --> 00:04.000 +This is a test. + +'''; + +const String _malformedVTT = ''' + +WEBVTT Kind: captions; Language: en + +00:09.000--> 00:11.430 +This one should be ignored because the arrow needs a space. + +00:13.000 --> 00:16.000 +Valid + +00:16.000 --> 00:8.000 +This one should be ignored because the time is missing a digit. + +'''; From 33ba6bc29dc5c4f83165869d4723a5e52b86275c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 22 Sep 2021 18:48:47 +0200 Subject: [PATCH 063/259] [camera_web] Update usage documentation (#4371) --- packages/camera/camera_web/CHANGELOG.md | 4 ++++ packages/camera/camera_web/README.md | 5 +++-- packages/camera/camera_web/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 8596b3595852..dd9225f48ff4 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1+1 + +* Update usage documentation. + ## 0.2.1 * Add video recording functionality. diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index 918e695496a4..04bf665c1039 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -8,8 +8,9 @@ The web implementation of [`camera`][camera]. ### Depend on the package -This package is not an [endorsed implementation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin) -of the camera plugin yet, so you'll need to [add it explicitly](https://pub.dev/packages/camera_web/install). +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `camera` +normally. This package will be automatically included in your app when you do. ## Example diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index f001fe92365b..f37500ad6e22 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/master/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.1 +version: 0.2.1+1 environment: sdk: ">=2.12.0 <3.0.0" From a1829ba7f5b2914865dfca4077edfcb0fe2ecc3b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 22 Sep 2021 13:58:07 -0400 Subject: [PATCH 064/259] Add false secret lists, and enforce ordering (#4372) --- .../google_maps_flutter_web/pubspec.yaml | 4 ++ .../google_sign_in/pubspec.yaml | 8 ++++ .../tool/lib/src/pubspec_check_command.dart | 2 + .../tool/test/pubspec_check_command_test.dart | 39 ++++++++++++++++++- 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 8a23916b0e98..08eda25352c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -31,3 +31,7 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.10.0 + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/web/index.html diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 79009373c5d1..c9931fd276f4 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -36,3 +36,11 @@ dev_dependencies: integration_test: sdk: flutter pedantic: ^1.10.0 + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/android/app/google-services.json + - /example/ios/Runner/GoogleService-Info.plist + - /example/ios/RunnerTests/GoogleSignInTests.m + - /example/lib/main.dart + - /example/web/index.html diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 605a8aa83a30..fec0dcef9ac7 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -39,6 +39,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'flutter:', 'dependencies:', 'dev_dependencies:', + 'false_secrets:', ]; static const List _majorPackageSections = [ @@ -46,6 +47,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'dependencies:', 'dev_dependencies:', 'flutter:', + 'false_secrets:', ]; static const String _expectedIssueLinkFormat = diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index c5d36013c40b..948136993d18 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -113,6 +113,13 @@ dev_dependencies: '''; } + String falseSecretsSection() { + return ''' +false_secrets: + - /lib/main.dart +'''; + } + test('passes for a plugin following conventions', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); @@ -122,6 +129,7 @@ ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} ${devDependenciesSection()} +${falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -147,6 +155,7 @@ ${environmentSection()} ${dependenciesSection()} ${devDependenciesSection()} ${flutterSection()} +${falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -399,7 +408,7 @@ ${dependenciesSection()} ); }); - test('fails when devDependencies section is out of order', () async { + test('fails when dev_dependencies section is out of order', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' @@ -426,6 +435,34 @@ ${dependenciesSection()} ); }); + test('fails when false_secrets section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${falseSecretsSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + test('fails when an implemenation package is missing "implements"', () async { final Directory pluginDirectory = createFakePlugin( From 44772b771a04591820cc076105a63067e5d02813 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 22 Sep 2021 15:58:07 -0700 Subject: [PATCH 065/259] [google_sign_in] Bump minimum Flutter version and iOS deployment target (#4334) --- packages/google_sign_in/google_sign_in/CHANGELOG.md | 4 ++++ packages/google_sign_in/google_sign_in/README.md | 2 ++ packages/google_sign_in/google_sign_in/example/pubspec.yaml | 4 ++-- .../google_sign_in/ios/google_sign_in.podspec | 2 +- packages/google_sign_in/google_sign_in/pubspec.yaml | 6 +++--- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 1f0be2e237b2..6107560ce610 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.1.1 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 5.1.0 * Add reAuthenticate option to signInSilently to allow re-authentication to be requested diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 6ed21c0fedd2..f3787474eeef 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -27,6 +27,8 @@ Otherwise, you may encounter `APIException` errors. ### iOS integration +This plugin requires iOS 9.0 or higher. + 1. [First register your application](https://firebase.google.com/docs/ios/setup). 2. Make sure the file you download in step 1 is named `GoogleService-Info.plist`. diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index 0379b9065333..dfd942d3d438 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Example of Google Sign-In plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec index a0b73276fafa..270ca274f3e4 100644 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec @@ -19,7 +19,7 @@ Enables Google Sign-In in Flutter apps. s.dependency 'GoogleSignIn', '~> 5.0' s.static_framework = true - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' # GoogleSignIn ~> 5.0 does not support arm64 simulators. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index c9931fd276f4..aa0a686776fb 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,11 +3,11 @@ 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/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.1.0 +version: 5.1.1 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: From 090e406cc9766cb071395cecc575f107936844d6 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 23 Sep 2021 13:28:05 +0200 Subject: [PATCH 066/259] [webview_flutter] Migrate main package to fully federated architecture. (#4366) --- .../webview_flutter/android/build.gradle | 57 -- .../webview_flutter/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 2 - .../webviewflutter/DisplayListenerProxy.java | 147 --- .../webviewflutter/FlutterCookieManager.java | 56 -- .../FlutterDownloadListener.java | 33 - .../webviewflutter/FlutterWebView.java | 498 ----------- .../webviewflutter/FlutterWebViewClient.java | 323 ------- .../webviewflutter/FlutterWebViewFactory.java | 33 - .../webviewflutter/InputAwareWebView.java | 233 ----- .../webviewflutter/JavaScriptChannel.java | 58 -- ...readedInputConnectionProxyAdapterView.java | 112 --- .../webviewflutter/WebViewBuilder.java | 155 ---- .../webviewflutter/WebViewFlutterPlugin.java | 73 -- .../FlutterDownloadListenerTest.java | 42 - .../FlutterWebViewClientTest.java | 60 -- .../webviewflutter/FlutterWebViewTest.java | 66 -- .../webviewflutter/WebViewBuilderTest.java | 104 --- .../plugins/webviewflutter/WebViewTest.java | 49 - .../webview_flutter_test.dart | 22 +- .../webview_flutter/ios/Assets/.gitkeep | 0 .../ios/Classes/FLTCookieManager.h | 14 - .../ios/Classes/FLTCookieManager.m | 44 - .../ios/Classes/FLTWKNavigationDelegate.h | 21 - .../ios/Classes/FLTWKNavigationDelegate.m | 116 --- .../ios/Classes/FLTWKProgressionDelegate.h | 19 - .../ios/Classes/FLTWKProgressionDelegate.m | 41 - .../ios/Classes/FLTWebViewFlutterPlugin.h | 8 - .../ios/Classes/FLTWebViewFlutterPlugin.m | 18 - .../ios/Classes/FlutterWebView.h | 32 - .../ios/Classes/FlutterWebView.m | 475 ---------- .../ios/Classes/JavaScriptChannelHandler.h | 17 - .../ios/Classes/JavaScriptChannelHandler.m | 36 - .../ios/webview_flutter.podspec | 23 - .../lib/platform_interface.dart | 564 +----------- .../webview_flutter/lib/src/webview.dart | 681 ++++++++++++++ .../lib/src/webview_android.dart | 60 -- .../lib/src/webview_cupertino.dart | 47 - .../lib/src/webview_method_channel.dart | 216 ----- .../webview_flutter/lib/webview_flutter.dart | 834 +----------------- .../webview_flutter/pubspec.yaml | 8 +- .../test/webview_flutter_test.dart | 3 +- 42 files changed, 725 insertions(+), 4676 deletions(-) delete mode 100644 packages/webview_flutter/webview_flutter/android/build.gradle delete mode 100644 packages/webview_flutter/webview_flutter/android/settings.gradle delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java delete mode 100644 packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java delete mode 100644 packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h delete mode 100644 packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m delete mode 100644 packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec create mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_android.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle deleted file mode 100644 index 4a164317c60f..000000000000 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -group 'io.flutter.plugins.webviewflutter' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 19 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.webkit:webkit:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.11.1' - testImplementation 'androidx.test:core:1.3.0' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/settings.gradle b/packages/webview_flutter/webview_flutter/android/settings.gradle deleted file mode 100644 index 5be7a4b4c692..000000000000 --- a/packages/webview_flutter/webview_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'webview_flutter' diff --git a/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index a087f2c75c24..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java deleted file mode 100644 index 31e3fe08c057..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ /dev/null @@ -1,147 +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.webviewflutter; - -import static android.hardware.display.DisplayManager.DisplayListener; - -import android.annotation.TargetApi; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Field; -import java.util.ArrayList; - -/** - * Works around an Android WebView bug by filtering some DisplayListener invocations. - * - *

Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged} - * is invoked, the display ID it is provided is of a valid display. However it turns out that when a - * display is removed Android may call onDisplayChanged with the ID of the removed display, in this - * case the Android WebView code tries to fetch and use the display with this ID and crashes with an - * NPE. - * - *

This issue was fixed in the Android WebView code in - * https://chromium-review.googlesource.com/517913 which is available starting WebView version - * 58.0.3029.125 however older webviews in the wild still have this issue. - * - *

Since Flutter removes virtual displays whenever a platform view is resized the webview crash - * is more likely to happen than other apps. And users were reporting this issue see: - * https://github.com/flutter/flutter/issues/30420 - * - *

This class works around the webview bug by unregistering the WebView's DisplayListener, and - * instead registering its own DisplayListener which delegates the callbacks to the WebView's - * listener unless it's a onDisplayChanged for an invalid display. - * - *

I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using - * reflection to fetch all registered listeners before and after initializing a webview. In the - * first initialization of a webview within the process the difference between the lists is the - * webview's display listener. - */ -@TargetApi(Build.VERSION_CODES.KITKAT) -class DisplayListenerProxy { - private static final String TAG = "DisplayListenerProxy"; - - private ArrayList listenersBeforeWebView; - - /** Should be called prior to the webview's initialization. */ - void onPreWebViewInitialization(DisplayManager displayManager) { - listenersBeforeWebView = yoinkDisplayListeners(displayManager); - } - - /** Should be called after the webview's initialization. */ - void onPostWebViewInitialization(final DisplayManager displayManager) { - final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); - // We recorded the list of listeners prior to initializing webview, any new listeners we see - // after initializing the webview are listeners added by the webview. - webViewListeners.removeAll(listenersBeforeWebView); - - if (webViewListeners.isEmpty()) { - // The Android WebView registers a single display listener per process (even if there - // are multiple WebView instances) so this list is expected to be non-empty only the - // first time a webview is initialized. - // Note that in an add2app scenario if the application had instantiated a non Flutter - // WebView prior to instantiating the Flutter WebView we are not able to get a reference - // to the WebView's display listener and can't work around the bug. - // - // This means that webview resizes in add2app Flutter apps with a non Flutter WebView - // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's - // behavior seems to be racy so it doesn't always happen). - return; - } - - for (DisplayListener webViewListener : webViewListeners) { - // Note that while DisplayManager.unregisterDisplayListener throws when given an - // unregistered listener, this isn't an issue as the WebView code never calls - // unregisterDisplayListener. - displayManager.unregisterDisplayListener(webViewListener); - - // We never explicitly unregister this listener as the webview's listener is never - // unregistered (it's released when the process is terminated). - displayManager.registerDisplayListener( - new DisplayListener() { - @Override - public void onDisplayAdded(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayAdded(displayId); - } - } - - @Override - public void onDisplayRemoved(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayRemoved(displayId); - } - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayManager.getDisplay(displayId) == null) { - return; - } - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayChanged(displayId); - } - } - }, - null); - } - } - - @SuppressWarnings({"unchecked", "PrivateApi"}) - private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // We cannot use reflection on Android P, but it shouldn't matter as it shipped - // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was - // fixed in 61.0.3116.0. - return new ArrayList<>(); - } - try { - Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); - displayManagerGlobalField.setAccessible(true); - Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); - Field displayListenersField = - displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); - displayListenersField.setAccessible(true); - ArrayList delegates = - (ArrayList) displayListenersField.get(displayManagerGlobal); - - Field listenerField = null; - ArrayList listeners = new ArrayList<>(); - for (Object delegate : delegates) { - if (listenerField == null) { - listenerField = delegate.getClass().getField("mListener"); - listenerField.setAccessible(true); - } - DisplayManager.DisplayListener listener = - (DisplayManager.DisplayListener) listenerField.get(delegate); - listeners.add(listener); - } - return listeners; - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.w(TAG, "Could not extract WebView's display listeners. " + e); - return new ArrayList<>(); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21daadeb..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +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.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java deleted file mode 100644 index cfad4e315514..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.webkit.DownloadListener; -import android.webkit.WebView; - -/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ -public class FlutterDownloadListener implements DownloadListener { - private final FlutterWebViewClient webViewClient; - private WebView webView; - - public FlutterDownloadListener(FlutterWebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ - public void setWebView(WebView webView) { - this.webView = webView; - } - - @Override - public void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - long contentLength) { - webViewClient.notifyDownload(webView, url); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index 4651a5f5ae22..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,498 +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.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - flutterWebViewClient.onLoadingProgress(progress); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - MethodChannel methodChannel, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - - FlutterDownloadListener flutterDownloadListener = - new FlutterDownloadListener(flutterWebViewClient); - webView = - createWebView( - new WebViewBuilder(context, containerView), - params, - new FlutterWebChromeClient(), - flutterDownloadListener); - flutterDownloadListener.setWebView(webView); - - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - - Map settings = (Map) params.get("settings"); - if (settings != null) { - applySettings(settings); - } - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) { - registerJavaScriptChannelNames(names); - } - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) { - updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - } - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - /** - * Creates a {@link android.webkit.WebView} and configures it according to the supplied - * parameters. - * - *

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

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

Important: This method is visible for testing purposes only and should - * never be called from outside this class. - * - * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link - * WebView}. - * @param params creation parameters received over the method channel. - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return The new {@link android.webkit.WebView} object. - */ - @VisibleForTesting - static WebView createWebView( - WebViewBuilder webViewBuilder, - Map params, - WebChromeClient webChromeClient, - @Nullable DownloadListener downloadListener) { - boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); - webViewBuilder - .setUsesHybridComposition(usesHybridComposition) - .setDomStorageEnabled(true) // Always enable DOM storage API. - .setJavaScriptCanOpenWindowsAutomatically( - true) // Always allow automatically opening of windows. - .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient(webChromeClient) - .setDownloadListener( - downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. - - return webViewBuilder.build(); - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).unlockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).lockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(flutterView); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(null); - } - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) { - updateJsMode(mode); - } - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "hasProgressTracking": - flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "allowsInlineMediaPlayback": - // no-op inline media playback is always allowed on Android. - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).dispose(); - } - webView.destroy(); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index 260ef8e8b15d..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,323 +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.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - boolean hasProgressTracking; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - /** - * Notifies the Flutter code that a download should start when a navigation delegate is set. - * - * @param view the webView the result of the navigation delegate will be send to. - * @param url the download url - * @return A boolean whether or not the request is forwarded to the Flutter code. - */ - boolean notifyDownload(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - void onLoadingProgress(int progress) { - if (hasProgressTracking) { - Map args = new HashMap<>(); - args.put("progress", progress); - methodChannel.invokeMethod("onProgress", args); - } - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java deleted file mode 100644 index 8fe58104a0fb..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.StandardMessageCodec; -import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; - -public final class FlutterWebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; - - FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { - super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; - } - - @SuppressWarnings("unchecked") - @Override - public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - return new FlutterWebView(context, methodChannel, params, containerView); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java deleted file mode 100644 index 51b2a3809fff..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ /dev/null @@ -1,233 +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.webviewflutter; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Build; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.ListPopupWindow; - -/** - * A WebView subclass that mirrors the same implementation hacks that the system WebView does in - * order to correctly create an InputConnection. - * - *

These hacks are only needed in Android versions below N and exist to create an InputConnection - * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in - * {@link #checkInputConnectionProxy}. - * - *

See also {@link ThreadedInputConnectionProxyAdapterView}. - */ -final class InputAwareWebView extends WebView { - private static final String TAG = "InputAwareWebView"; - private View threadedInputConnectionProxyView; - private ThreadedInputConnectionProxyAdapterView proxyAdapterView; - private View containerView; - - InputAwareWebView(Context context, View containerView) { - super(context); - this.containerView = containerView; - } - - void setContainerView(View containerView) { - this.containerView = containerView; - - if (proxyAdapterView == null) { - return; - } - - Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); - if (containerView != null) { - setInputConnectionTarget(proxyAdapterView); - } - } - - /** - * Set our proxy adapter view to use its cached input connection instead of creating new ones. - * - *

This is used to avoid losing our input connection when the virtual display is resized. - */ - void lockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(true); - } - - /** Sets the proxy adapter view back to its default behavior. */ - void unlockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(false); - } - - /** Restore the original InputConnection, if needed. */ - void dispose() { - resetInputConnection(); - } - - /** - * Creates an InputConnection from the IME thread when needed. - * - *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an - * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the - * system calling this method for WebView's proxy view in order to know when we need to create our - * own. - * - *

This method would normally be called for any View that used the InputMethodManager. We rely - * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the - * system WebView in order to know whether or not the system WebView expects an InputConnection on - * the IME thread. - */ - @Override - public boolean checkInputConnectionProxy(final View view) { - // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. - View previousProxy = threadedInputConnectionProxyView; - threadedInputConnectionProxyView = view; - if (previousProxy == view) { - // This isn't a new ThreadedInputConnectionProxyView. Ignore it. - return super.checkInputConnectionProxy(view); - } - if (containerView == null) { - Log.e( - TAG, - "Can't create a proxy view because there's no container view. Text input may not work."); - return super.checkInputConnectionProxy(view); - } - - // We've never seen this before, so we make the assumption that this is WebView's - // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could - // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. - proxyAdapterView = - new ThreadedInputConnectionProxyAdapterView( - /*containerView=*/ containerView, - /*targetView=*/ view, - /*imeHandler=*/ view.getHandler()); - setInputConnectionTarget(/*targetView=*/ proxyAdapterView); - return super.checkInputConnectionProxy(view); - } - - /** - * Ensure that input creation happens back on {@link #containerView}'s thread once this view no - * longer has focus. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - @Override - public void clearFocus() { - super.clearFocus(); - resetInputConnection(); - } - - /** - * Ensure that input creation happens back on {@link #containerView}. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - private void resetInputConnection() { - if (proxyAdapterView == null) { - // No need to reset the InputConnection to the default thread if we've never changed it. - return; - } - if (containerView == null) { - Log.e(TAG, "Can't reset the input connection to the container view because there is none."); - return; - } - setInputConnectionTarget(/*targetView=*/ containerView); - } - - /** - * This is the crucial trick that gets the InputConnection creation to happen on the correct - * thread pre Android N. - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a - * - *

{@code targetView} should have a {@link View#getHandler} method with the thread that future - * InputConnections should be created on. - */ - private void setInputConnectionTarget(final View targetView) { - if (containerView == null) { - Log.e( - TAG, - "Can't set the input connection target because there is no containerView to use as a handler."); - return; - } - - targetView.requestFocus(); - containerView.post( - new Runnable() { - @Override - public void run() { - InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); - // This is a hack to make InputMethodManager believe that the target view now has focus. - // As a result, InputMethodManager will think that targetView is focused, and will call - // getHandler() of the view when creating input connection. - - // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect - // the real window focus. - targetView.onWindowFocusChanged(true); - - // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call - // onCreateInputConnection() on targetView on the same thread as - // targetView.getHandler(). It will also call subsequent InputConnection methods on this - // thread. This is the IME thread in cases where targetView is our proxyAdapterView. - imm.isActive(containerView); - } - }); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - // This works around a crash when old (<67.0.3367.0) Chromium versions are used. - - // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown - // on tablets: - // - // - WebView is calling ListPopupWindow#show - // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. - // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is - // also synchronously performing the following sequence: - // - WebView's focus change listener is loosing focus (as mDropDownList got it) - // - WebView is hiding all popups (as it lost focus) - // - WebView's SelectPopupDropDown#hide is invoked. - // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. - // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). - // - // To workaround this, we drop the problematic focus lost call. - // See more details on: https://github.com/flutter/flutter/issues/54164 - // - // We don't do this after Android P as it shipped with a new enough WebView version, and it's - // better to not do this on all future Android versions in case DropDownListView's code changes. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && isCalledFromListPopupWindowShow() - && !focused) { - return; - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - private boolean isCalledFromListPopupWindowShow() { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (StackTraceElement stackTraceElement : stackTraceElements) { - if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) - && stackTraceElement.getMethodName().equals("show")) { - return true; - } - } - return false; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java deleted file mode 100644 index 4d596351b3d0..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ /dev/null @@ -1,58 +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.webviewflutter; - -import android.os.Handler; -import android.os.Looper; -import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; - -/** - * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets - * up. - * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. - */ -class JavaScriptChannel { - private final MethodChannel methodChannel; - private final String javaScriptChannelName; - private final Handler platformThreadHandler; - - /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through - */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; - this.platformThreadHandler = platformThreadHandler; - } - - // Suppressing unused warning as this is invoked from JavaScript. - @SuppressWarnings("unused") - @JavascriptInterface - public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java deleted file mode 100644 index 1c865c9444e2..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java +++ /dev/null @@ -1,112 +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.webviewflutter; - -import android.os.Handler; -import android.os.IBinder; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -/** - * A fake View only exposed to InputMethodManager. - * - *

This follows a similar flow to Chromium's WebView (see - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). - * WebView itself bounces its InputConnection around several different threads. We follow its logic - * here to get the same working connection. - * - *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on - * the IME thread. The way that this is created in {@link - * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to - * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME - * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. - */ -final class ThreadedInputConnectionProxyAdapterView extends View { - final Handler imeHandler; - final IBinder windowToken; - final View containerView; - final View rootView; - final View targetView; - - private boolean triggerDelayed = true; - private boolean isLocked = false; - private InputConnection cachedConnection; - - ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { - super(containerView.getContext()); - this.imeHandler = imeHandler; - this.containerView = containerView; - this.targetView = targetView; - windowToken = containerView.getWindowToken(); - rootView = containerView.getRootView(); - setFocusable(true); - setFocusableInTouchMode(true); - setVisibility(VISIBLE); - } - - /** Returns whether or not this is currently asynchronously acquiring an input connection. */ - boolean isTriggerDelayed() { - return triggerDelayed; - } - - /** Sets whether or not this should use its previously cached input connection. */ - void setLocked(boolean locked) { - isLocked = locked; - } - - /** - * This is expected to be called on the IME thread. See the setup required for this in {@link - * InputAwareWebView#checkInputConnectionProxy(View)}. - * - *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. - */ - @Override - public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { - triggerDelayed = false; - InputConnection inputConnection = - (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); - triggerDelayed = true; - cachedConnection = inputConnection; - return inputConnection; - } - - @Override - public boolean checkInputConnectionProxy(View view) { - return true; - } - - @Override - public boolean hasWindowFocus() { - // None of our views here correctly report they have window focus because of how we're embedding - // the platform view inside of a virtual display. - return true; - } - - @Override - public View getRootView() { - return rootView; - } - - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean isFocused() { - return true; - } - - @Override - public IBinder getWindowToken() { - return windowToken; - } - - @Override - public Handler getHandler() { - return imeHandler; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java deleted file mode 100644 index d3cd1d57cdae..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ /dev/null @@ -1,155 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** Builder used to create {@link android.webkit.WebView} objects. */ -public class WebViewBuilder { - - /** Factory used to create a new {@link android.webkit.WebView} instance. */ - static class WebViewFactory { - - /** - * Creates a new {@link android.webkit.WebView} instance. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is - * returned. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set - * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or - * IME, thread (see also {@link InputAwareWebView}) - * @return A new instance of the {@link android.webkit.WebView} object. - */ - static WebView create(Context context, boolean usesHybridComposition, View containerView) { - return usesHybridComposition - ? new WebView(context) - : new InputAwareWebView(context, containerView); - } - } - - private final Context context; - private final View containerView; - - private boolean enableDomStorage; - private boolean javaScriptCanOpenWindowsAutomatically; - private boolean supportMultipleWindows; - private boolean usesHybridComposition; - private WebChromeClient webChromeClient; - private DownloadListener downloadListener; - - /** - * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link - * WebViewFactory} object. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to - * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, - * thread (see also {@link InputAwareWebView}) - */ - WebViewBuilder(@NonNull final Context context, View containerView) { - this.context = context; - this.containerView = containerView; - } - - /** - * Sets whether the DOM storage API is enabled. The default value is {@code false}. - * - * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDomStorageEnabled(boolean flag) { - this.enableDomStorage = flag; - return this; - } - - /** - * Sets whether JavaScript is allowed to open windows automatically. This applies to the - * JavaScript function {@code window.open()}. The default value is {@code false}. - * - * @param flag {@code true} if JavaScript is allowed to open windows automatically. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { - this.javaScriptCanOpenWindowsAutomatically = flag; - return this; - } - - /** - * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link - * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is - * {@code false}. - * - * @param flag {@code true} if multiple windows are supported. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setSupportMultipleWindows(boolean flag) { - this.supportMultipleWindows = flag; - return this; - } - - /** - * Sets whether the hybrid composition should be used. - * - *

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

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

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

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

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

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(registrar.messenger(), registrar.view())); - new FlutterCookieManager(registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java deleted file mode 100644 index 2c918584ba83..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java +++ /dev/null @@ -1,42 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.webkit.WebView; -import org.junit.Before; -import org.junit.Test; - -public class FlutterDownloadListenerTest { - private FlutterWebViewClient webViewClient; - private WebView webView; - - @Before - public void before() { - webViewClient = mock(FlutterWebViewClient.class); - webView = mock(WebView.class); - } - - @Test - public void onDownloadStart_should_notify_webViewClient() { - String url = "testurl.com"; - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); - } - - @Test - public void onDownloadStart_should_pass_webView() { - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.setWebView(webView); - downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(eq(webView), anyString()); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java deleted file mode 100644 index 86346ac08f16..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ /dev/null @@ -1,60 +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.webviewflutter; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import android.webkit.WebView; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class FlutterWebViewClientTest { - - MethodChannel mockMethodChannel; - WebView mockWebView; - - @Before - public void before() { - mockMethodChannel = mock(MethodChannel.class); - mockWebView = mock(WebView.class); - } - - @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(true); - - client.notifyDownload(mockWebView, url); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(mockMethodChannel) - .invokeMethod( - eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); - HashMap map = (HashMap) argumentCaptor.getValue(); - assertEquals(map.get("url"), url); - assertEquals(map.get("isForMainFrame"), true); - } - - @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(false); - - client.notifyDownload(mockWebView, url); - verifyNoInteractions(mockMethodChannel); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java deleted file mode 100644 index 56d9db1ee493..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ /dev/null @@ -1,66 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; - -public class FlutterWebViewTest { - private WebChromeClient mockWebChromeClient; - private DownloadListener mockDownloadListener; - private WebViewBuilder mockWebViewBuilder; - private WebView mockWebView; - - @Before - public void before() { - mockWebChromeClient = mock(WebChromeClient.class); - mockWebViewBuilder = mock(WebViewBuilder.class); - mockWebView = mock(WebView.class); - mockDownloadListener = mock(DownloadListener.class); - - when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) - .thenReturn(mockWebViewBuilder); - - when(mockWebViewBuilder.build()).thenReturn(mockWebView); - } - - @Test - public void createWebView_should_create_webview_with_default_configuration() { - FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); - - verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); - verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); - verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); - verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); - } - - private Map createParameterMap(boolean usesHybridComposition) { - Map params = new HashMap<>(); - params.put("usesHybridComposition", usesHybridComposition); - - return params; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java deleted file mode 100644 index 423cb210c392..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ /dev/null @@ -1,104 +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.webviewflutter; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; -import java.io.IOException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; -import org.mockito.MockedStatic.Verification; - -public class WebViewBuilderTest { - private Context mockContext; - private View mockContainerView; - private WebView mockWebView; - private MockedStatic mockedStaticWebViewFactory; - - @Before - public void before() { - mockContext = mock(Context.class); - mockContainerView = mock(View.class); - mockWebView = mock(WebView.class); - mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); - - mockedStaticWebViewFactory - .when( - new Verification() { - @Override - public void apply() { - WebViewFactory.create(mockContext, false, mockContainerView); - } - }) - .thenReturn(mockWebView); - } - - @After - public void after() { - mockedStaticWebViewFactory.close(); - } - - @Test - public void ctor_test() { - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - assertNotNull(builder); - } - - @Test - public void build_should_set_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - DownloadListener mockDownloadListener = mock(DownloadListener.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = - new WebViewBuilder(mockContext, mockContainerView) - .setDomStorageEnabled(true) - .setJavaScriptCanOpenWindowsAutomatically(true) - .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient) - .setDownloadListener(mockDownloadListener); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(true); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebSettings).setSupportMultipleWindows(true); - verify(mockWebView).setWebChromeClient(mockWebChromeClient); - verify(mockWebView).setDownloadListener(mockDownloadListener); - } - - @Test - public void build_should_use_default_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(false); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); - verify(mockWebSettings).setSupportMultipleWindows(false); - verify(mockWebView).setWebChromeClient(null); - verify(mockWebView).setDownloadListener(null); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java deleted file mode 100644 index 131a5a3eb53a..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ /dev/null @@ -1,49 +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.webviewflutter; - -import static org.junit.Assert.assertEquals; - -import android.webkit.WebViewClient; -import org.junit.Test; - -public class WebViewTest { - @Test - public void errorCodes() { - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), - "authentication"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), - "failedSslHandshake"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), - "proxyAuthentication"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), - "tooManyRequests"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), - "unsafeResource"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), - "unsupportedAuthScheme"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), - "unsupportedScheme"); - } -} diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 0e128caa8f32..a6211b2dae75 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 @@ -18,6 +18,8 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const bool _skipDueToIssue86757 = true; + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = @@ -37,7 +39,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); - }, skip: true); + }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl', (WidgetTester tester) async { @@ -59,7 +61,7 @@ void main() { await controller.loadUrl('https://www.google.com/'); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://www.google.com/'); - }, skip: true); + }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { @@ -101,7 +103,7 @@ void main() { final String content = await controller .evaluateJavascript('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('JavaScriptChannel', (WidgetTester tester) async { @@ -150,7 +152,7 @@ void main() { // https://github.com/flutter/flutter/issues/66318 await controller.evaluateJavascript('Echo.postMessage("hello");1;'); expect(messagesReceived, equals(['hello'])); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { final String resizeTest = ''' @@ -328,7 +330,7 @@ void main() { final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); group('Video playback policy', () { late String videoTestBase64; @@ -877,7 +879,7 @@ void main() { scrollPosY = await controller.getScrollY(); expect(scrollPosX, X_SCROLL * 2); expect(scrollPosY, Y_SCROLL * 2); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); }); group('SurfaceAndroidWebView', () { @@ -956,7 +958,7 @@ void main() { scrollPosY = await controller.getScrollY(); expect(X_SCROLL * 2, scrollPosX); expect(Y_SCROLL * 2, scrollPosY); - }, skip: true); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('inputs are scrolled into view when focused', @@ -1062,7 +1064,7 @@ void main() { lastInputClientRectRelativeToViewport['right'] <= viewportRectRelativeToViewport['right'], isTrue); - }, skip: true); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); }); group('NavigationDelegate', () { @@ -1332,7 +1334,7 @@ void main() { expect(currentUrl, 'https://flutter.dev/'); }, // Flaky on Android: https://github.com/flutter/flutter/issues/86757 - skip: Platform.isAndroid); + skip: Platform.isAndroid && _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( @@ -1373,7 +1375,7 @@ void main() { await pageLoaded.future; expect(controller.currentUrl(), completion('https://flutter.dev/')); }, - skip: true, + skip: _skipDueToIssue86757, ); testWidgets( diff --git a/packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h deleted file mode 100644 index 8fe331875250..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTCookieManager : NSObject - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m deleted file mode 100644 index eb7c856b250d..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTCookieManager.h" - -@implementation FLTCookieManager { -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTCookieManager *instance = [[FLTCookieManager alloc] init]; - - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"clearCookies"]) { - [self clearCookies:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)clearCookies:(FlutterResult)result { - NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - - void (^deleteAndNotify)(NSArray *) = - ^(NSArray *cookies) { - BOOL hasCookies = cookies.count > 0; - [dataStore removeDataOfTypes:websiteDataTypes - forDataRecords:cookies - completionHandler:^{ - result(@(hasCookies)); - }]; - }; - - [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h deleted file mode 100644 index 31edadc8cc05..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWKNavigationDelegate : NSObject - -- (instancetype)initWithChannel:(FlutterMethodChannel*)channel; - -/** - * Whether to delegate navigation decisions over the method channel. - */ -@property(nonatomic, assign) BOOL hasDartNavigationDelegate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m deleted file mode 100644 index 8b7ee7d0cfb7..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTWKNavigationDelegate.h" - -@implementation FLTWKNavigationDelegate { - FlutterMethodChannel *_methodChannel; -} - -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _methodChannel = channel; - } - return self; -} - -#pragma mark - WKNavigationDelegate conformance - -- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { - [_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}]; -} - -- (void)webView:(WKWebView *)webView - decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction - decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - if (!self.hasDartNavigationDelegate) { - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - NSDictionary *arguments = @{ - @"url" : navigationAction.request.URL.absoluteString, - @"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame) - }; - [_methodChannel invokeMethod:@"navigationRequest" - arguments:arguments - result:^(id _Nullable result) { - if ([result isKindOfClass:[FlutterError class]]) { - NSLog(@"navigationRequest has unexpectedly completed with an error, " - @"allowing navigation."); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - if (result == FlutterMethodNotImplemented) { - NSLog(@"navigationRequest was unexepectedly not implemented: %@, " - @"allowing navigation.", - result); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - if (![result isKindOfClass:[NSNumber class]]) { - NSLog(@"navigationRequest unexpectedly returned a non boolean value: " - @"%@, allowing navigation.", - result); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - NSNumber *typedResult = result; - decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow - : WKNavigationActionPolicyCancel); - }]; -} - -- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - [_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}]; -} - -+ (id)errorCodeToString:(NSUInteger)code { - switch (code) { - case WKErrorUnknown: - return @"unknown"; - case WKErrorWebContentProcessTerminated: - return @"webContentProcessTerminated"; - case WKErrorWebViewInvalidated: - return @"webViewInvalidated"; - case WKErrorJavaScriptExceptionOccurred: - return @"javaScriptExceptionOccurred"; - case WKErrorJavaScriptResultTypeIsUnsupported: - return @"javaScriptResultTypeIsUnsupported"; - } - - return [NSNull null]; -} - -- (void)onWebResourceError:(NSError *)error { - [_methodChannel invokeMethod:@"onWebResourceError" - arguments:@{ - @"errorCode" : @(error.code), - @"domain" : error.domain, - @"description" : error.description, - @"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code], - }]; -} - -- (void)webView:(WKWebView *)webView - didFailNavigation:(WKNavigation *)navigation - withError:(NSError *)error { - [self onWebResourceError:error]; -} - -- (void)webView:(WKWebView *)webView - didFailProvisionalNavigation:(WKNavigation *)navigation - withError:(NSError *)error { - [self onWebResourceError:error]; -} - -- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { - NSError *contentProcessTerminatedError = - [[NSError alloc] initWithDomain:WKErrorDomain - code:WKErrorWebContentProcessTerminated - userInfo:nil]; - [self onWebResourceError:contentProcessTerminatedError]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h deleted file mode 100644 index 96af4ef6c578..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWKProgressionDelegate : NSObject - -- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; - -- (void)stopObservingProgress:(WKWebView *)webView; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m deleted file mode 100644 index 8e7af4649aa0..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTWKProgressionDelegate.h" - -NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; - -@implementation FLTWKProgressionDelegate { - FlutterMethodChannel *_methodChannel; -} - -- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _methodChannel = channel; - [webView addObserver:self - forKeyPath:FLTWKEstimatedProgressKeyPath - options:NSKeyValueObservingOptionNew - context:nil]; - } - return self; -} - -- (void)stopObservingProgress:(WKWebView *)webView { - [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { - NSNumber *newValue = - change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 - int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 - [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; - } -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h deleted file mode 100644 index 2a80c7d886f2..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTWebViewFlutterPlugin : NSObject -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m deleted file mode 100644 index 9f01416acc6a..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTWebViewFlutterPlugin.h" -#import "FLTCookieManager.h" -#import "FlutterWebView.h" - -@implementation FLTWebViewFlutterPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTWebViewFactory* webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; - [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; - [FLTCookieManager registerWithRegistrar:registrar]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h deleted file mode 100644 index 6e795f7d1528..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWebViewController : NSObject - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger; - -- (UIView*)view; -@end - -@interface FLTWebViewFactory : NSObject -- (instancetype)initWithMessenger:(NSObject*)messenger; -@end - -/** - * The WkWebView used for the plugin. - * - * This class overrides some methods in `WKWebView` to serve the needs for the plugin. - */ -@interface FLTWKWebView : WKWebView -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m deleted file mode 100644 index 1604f2756f31..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m +++ /dev/null @@ -1,475 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FlutterWebView.h" -#import "FLTWKNavigationDelegate.h" -#import "FLTWKProgressionDelegate.h" -#import "JavaScriptChannelHandler.h" - -@implementation FLTWebViewFactory { - NSObject* _messenger; -} - -- (instancetype)initWithMessenger:(NSObject*)messenger { - self = [super init]; - if (self) { - _messenger = messenger; - } - return self; -} - -- (NSObject*)createArgsCodec { - return [FlutterStandardMessageCodec sharedInstance]; -} - -- (NSObject*)createWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args { - FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame - viewIdentifier:viewId - arguments:args - binaryMessenger:_messenger]; - return webviewController; -} - -@end - -@implementation FLTWKWebView - -- (void)setFrame:(CGRect)frame { - [super setFrame:frame]; - self.scrollView.contentInset = UIEdgeInsetsZero; - // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of - // webview's contentInsets. - // 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); - } -} - -@end - -@implementation FLTWebViewController { - FLTWKWebView* _webView; - int64_t _viewId; - FlutterMethodChannel* _channel; - NSString* _currentUrl; - // The set of registered JavaScript channel names. - NSMutableSet* _javaScriptChannelNames; - FLTWKNavigationDelegate* _navigationDelegate; - FLTWKProgressionDelegate* _progressionDelegate; -} - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger { - if (self = [super init]) { - _viewId = viewId; - - NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; - _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; - _javaScriptChannelNames = [[NSMutableSet alloc] init]; - - WKUserContentController* userContentController = [[WKUserContentController alloc] init]; - if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { - NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"]; - [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; - [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; - } - - NSDictionary* settings = args[@"settings"]; - - WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; - [self applyConfigurationSettings:settings toConfiguration:configuration]; - configuration.userContentController = userContentController; - [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] - inConfiguration:configuration]; - - _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; - _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; - _webView.UIDelegate = self; - _webView.navigationDelegate = _navigationDelegate; - __weak __typeof__(self) weakSelf = self; - [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - [weakSelf onMethodCall:call result:result]; - }]; - - if (@available(iOS 11.0, *)) { - _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - if (@available(iOS 13.0, *)) { - _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; - } - } - - [self applySettings:settings]; - // TODO(amirh): return an error if apply settings failed once it's possible to do so. - // https://github.com/flutter/flutter/issues/36228 - - NSString* initialUrl = args[@"initialUrl"]; - if ([initialUrl isKindOfClass:[NSString class]]) { - [self loadUrl:initialUrl]; - } - } - return self; -} - -- (void)dealloc { - if (_progressionDelegate != nil) { - [_progressionDelegate stopObservingProgress:_webView]; - } -} - -- (UIView*)view { - return _webView; -} - -- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"updateSettings"]) { - [self onUpdateSettings:call result:result]; - } else if ([[call method] isEqualToString:@"loadUrl"]) { - [self onLoadUrl:call result:result]; - } else if ([[call method] isEqualToString:@"canGoBack"]) { - [self onCanGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"canGoForward"]) { - [self onCanGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"goBack"]) { - [self onGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"goForward"]) { - [self onGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"reload"]) { - [self onReload:call result:result]; - } else if ([[call method] isEqualToString:@"currentUrl"]) { - [self onCurrentUrl:call result:result]; - } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { - [self onEvaluateJavaScript:call result:result]; - } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { - [self onAddJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { - [self onRemoveJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"clearCache"]) { - [self clearCache:result]; - } else if ([[call method] isEqualToString:@"getTitle"]) { - [self onGetTitle:result]; - } else if ([[call method] isEqualToString:@"scrollTo"]) { - [self onScrollTo:call result:result]; - } else if ([[call method] isEqualToString:@"scrollBy"]) { - [self onScrollBy:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollX"]) { - [self getScrollX:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollY"]) { - [self getScrollY:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* error = [self applySettings:[call arguments]]; - if (error == nil) { - result(nil); - return; - } - result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); -} - -- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - if (![self loadRequest:[call arguments]]) { - result([FlutterError - errorWithCode:@"loadUrl_failed" - message:@"Failed parsing the URL" - details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); - } else { - result(nil); - } -} - -- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoBack = [_webView canGoBack]; - result(@(canGoBack)); -} - -- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoForward = [_webView canGoForward]; - result(@(canGoForward)); -} - -- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goBack]; - result(nil); -} - -- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goForward]; - result(nil); -} - -- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView reload]; - result(nil); -} - -- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - _currentUrl = [[_webView URL] absoluteString]; - result(_currentUrl); -} - -- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* jsString = [call arguments]; - if (!jsString) { - result([FlutterError errorWithCode:@"evaluateJavaScript_failed" - message:@"JavaScript String cannot be null" - details:nil]); - return; - } - [_webView evaluateJavaScript:jsString - completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { - if (error) { - result([FlutterError - errorWithCode:@"evaluateJavaScript_failed" - message:@"Failed evaluating JavaScript" - details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", - jsString, error]]); - } else { - result([NSString stringWithFormat:@"%@", evaluateResult]); - } - }]; -} - -- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - NSArray* channelNames = [call arguments]; - NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; - [_javaScriptChannelNames addObjectsFromArray:channelNames]; - [self registerJavaScriptChannels:channelNamesSet - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - // WkWebView does not support removing a single user script, so instead we remove all - // user scripts, all message handlers. And re-register channels that shouldn't be removed. - [_webView.configuration.userContentController removeAllUserScripts]; - for (NSString* channelName in _javaScriptChannelNames) { - [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; - } - - NSArray* channelNamesToRemove = [call arguments]; - for (NSString* channelName in channelNamesToRemove) { - [_javaScriptChannelNames removeObject:channelName]; - } - - [self registerJavaScriptChannels:_javaScriptChannelNames - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)clearCache:(FlutterResult)result { - 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 { - NSString* title = _webView.title; - result(title); -} - -- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result { - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue]; - int y = [arguments[@"y"] intValue]; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { - CGPoint contentOffset = _webView.scrollView.contentOffset; - - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue] + contentOffset.x; - int y = [arguments[@"y"] intValue] + contentOffset.y; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetX = _webView.scrollView.contentOffset.x; - result(@(offsetX)); -} - -- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetY = _webView.scrollView.contentOffset.y; - result(@(offsetY)); -} - -// Returns nil when successful, or an error message when one or more keys are unknown. -- (NSString*)applySettings:(NSDictionary*)settings { - NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; - for (NSString* key in settings) { - if ([key isEqualToString:@"jsMode"]) { - NSNumber* mode = settings[key]; - [self updateJsMode:mode]; - } else if ([key isEqualToString:@"hasNavigationDelegate"]) { - NSNumber* hasDartNavigationDelegate = settings[key]; - _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; - } else if ([key isEqualToString:@"hasProgressTracking"]) { - NSNumber* hasProgressTrackingValue = settings[key]; - bool hasProgressTracking = [hasProgressTrackingValue boolValue]; - if (hasProgressTracking) { - _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView - channel:_channel]; - } - } else if ([key isEqualToString:@"debuggingEnabled"]) { - // no-op debugging is always enabled on iOS. - } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { - NSNumber* allowsBackForwardNavigationGestures = settings[key]; - _webView.allowsBackForwardNavigationGestures = - [allowsBackForwardNavigationGestures boolValue]; - } else if ([key isEqualToString:@"userAgent"]) { - NSString* userAgent = settings[key]; - [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; - } else { - [unknownKeys addObject:key]; - } - } - if ([unknownKeys count] == 0) { - return nil; - } - return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", - [unknownKeys componentsJoinedByString:@", "]]; -} - -- (void)applyConfigurationSettings:(NSDictionary*)settings - toConfiguration:(WKWebViewConfiguration*)configuration { - NSAssert(configuration != _webView.configuration, - @"configuration needs to be updated before webView.configuration."); - for (NSString* key in settings) { - if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { - NSNumber* allowsInlineMediaPlayback = settings[key]; - configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; - } - } -} - -- (void)updateJsMode:(NSNumber*)mode { - WKPreferences* preferences = [[_webView configuration] preferences]; - switch ([mode integerValue]) { - case 0: // disabled - [preferences setJavaScriptEnabled:NO]; - break; - case 1: // unrestricted - [preferences setJavaScriptEnabled:YES]; - break; - default: - NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); - } -} - -- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy - inConfiguration:(WKWebViewConfiguration*)configuration { - switch ([policy integerValue]) { - case 0: // require_user_action_for_all_media_types - if (@available(iOS 10.0, *)) { - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - } else { - configuration.requiresUserActionForMediaPlayback = true; - } - break; - case 1: // always_allow - if (@available(iOS 10.0, *)) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; -#pragma clang diagnostic pop - } else { - configuration.requiresUserActionForMediaPlayback = false; - } - break; - default: - NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); - } -} - -- (bool)loadRequest:(NSDictionary*)request { - if (!request) { - return false; - } - - NSString* url = request[@"url"]; - if ([url isKindOfClass:[NSString class]]) { - id headers = request[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - return [self loadUrl:url withHeaders:headers]; - } else { - return [self loadUrl:url]; - } - } - - return false; -} - -- (bool)loadUrl:(NSString*)url { - return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; -} - -- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; - } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; - [request setAllHTTPHeaderFields:headers]; - [_webView loadRequest:request]; - return true; -} - -- (void)registerJavaScriptChannels:(NSSet*)channelNames - controller:(WKUserContentController*)userContentController { - for (NSString* channelName in channelNames) { - FLTJavaScriptChannel* channel = - [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel - javaScriptChannelName:channelName]; - [userContentController addScriptMessageHandler:channel name:channelName]; - NSString* wrapperSource = [NSString - stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; - WKUserScript* wrapperScript = - [[WKUserScript alloc] initWithSource:wrapperSource - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:NO]; - [userContentController addUserScript:wrapperScript]; - } -} - -- (void)updateUserAgent:(NSString*)userAgent { - [_webView setCustomUserAgent:userAgent]; -} - -#pragma mark WKUIDelegate - -- (WKWebView*)webView:(WKWebView*)webView - createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration - forNavigationAction:(WKNavigationAction*)navigationAction - windowFeatures:(WKWindowFeatures*)windowFeatures { - if (!navigationAction.targetFrame.isMainFrame) { - [webView loadRequest:navigationAction.request]; - } - - return nil; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h deleted file mode 100644 index a0a5ec657295..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTJavaScriptChannel : NSObject - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel - javaScriptChannelName:(NSString*)javaScriptChannelName; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m deleted file mode 100644 index ec9a363a4b2e..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "JavaScriptChannelHandler.h" - -@implementation FLTJavaScriptChannel { - FlutterMethodChannel* _methodChannel; - NSString* _javaScriptChannelName; -} - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel - javaScriptChannelName:(NSString*)javaScriptChannelName { - self = [super init]; - NSAssert(methodChannel != nil, @"methodChannel must not be null."); - NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null."); - if (self) { - _methodChannel = methodChannel; - _javaScriptChannelName = javaScriptChannelName; - } - return self; -} - -- (void)userContentController:(WKUserContentController*)userContentController - didReceiveScriptMessage:(WKScriptMessage*)message { - NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel."); - NSAssert(_javaScriptChannelName != nil, - @"Can't send a message to an unitialized JavaScript channel."); - NSDictionary* arguments = @{ - @"channel" : _javaScriptChannelName, - @"message" : [NSString stringWithFormat:@"%@", message.body] - }; - [_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec b/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec deleted file mode 100644 index 2e021994b8f4..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'webview_flutter' - s.version = '0.0.1' - s.summary = 'A WebView Plugin for Flutter.' - s.description = <<-DESC -A Flutter plugin that provides a WebView widget. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter' } - s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } -end diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart index 92aa87b7480f..aa7b3a0931e8 100644 --- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart @@ -2,547 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'webview_flutter.dart'; - -/// Interface for callbacks made by [WebViewPlatformController]. -/// -/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. -/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. -abstract class WebViewPlatformCallbacksHandler { - /// Invoked by [WebViewPlatformController] when a JavaScript channel message is received. - void onJavaScriptChannelMessage(String channel, String message); - - /// Invoked by [WebViewPlatformController] when a navigation request is pending. - /// - /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest( - {required String url, required bool isForMainFrame}); - - /// Invoked by [WebViewPlatformController] when a page has started loading. - void onPageStarted(String url); - - /// Invoked by [WebViewPlatformController] when a page has finished loading. - void onPageFinished(String url); - - /// Invoked by [WebViewPlatformController] when a page is loading. - /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. - void onProgress(int progress); - - /// Report web resource loading error to the host application. - void onWebResourceError(WebResourceError error); -} - -/// Possible error type categorizations used by [WebResourceError]. -enum WebResourceErrorType { - /// User authentication failed on server. - authentication, - - /// Malformed URL. - badUrl, - - /// Failed to connect to the server. - connect, - - /// Failed to perform SSL handshake. - failedSslHandshake, - - /// Generic file error. - file, - - /// File not found. - fileNotFound, - - /// Server or proxy hostname lookup failed. - hostLookup, - - /// Failed to read or write to the server. - io, - - /// User authentication failed on proxy. - proxyAuthentication, - - /// Too many redirects. - redirectLoop, - - /// Connection timed out. - timeout, - - /// Too many requests during this load. - tooManyRequests, - - /// Generic error. - unknown, - - /// Resource load was canceled by Safe Browsing. - unsafeResource, - - /// Unsupported authentication scheme (not basic or digest). - unsupportedAuthScheme, - - /// Unsupported URI scheme. - unsupportedScheme, - - /// The web content process was terminated. - webContentProcessTerminated, - - /// The web view was invalidated. - webViewInvalidated, - - /// A JavaScript exception occurred. - javaScriptExceptionOccurred, - - /// The result of JavaScript execution could not be returned. - javaScriptResultTypeIsUnsupported, -} - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ - required this.errorCode, - required this.description, - this.domain, - this.errorType, - this.failingUrl, - }) : assert(errorCode != null), - assert(description != null); - - /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. - final int errorCode; - - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String? domain; - - /// Description of the error that can be used to communicate the problem to the user. - final String description; - - /// The type this error can be categorized as. - /// - /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType? errorType; - - /// Gets the URL for which the resource request was made. - /// - /// This value is not provided on iOS. Alternatively, you can keep track of - /// the last values provided to [WebViewPlatformController.loadUrl]. - final String? failingUrl; -} - -/// Interface for talking to the webview's platform implementation. -/// -/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is -/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. -/// -/// Platform implementations that live in a separate package should extend this class rather than -/// implement it as webview_flutter does not consider newly added methods to be breaking changes. -/// Extending this class (using `extends`) ensures that the subclass will get the default -/// implementation, while platform implementations that `implements` this interface will be broken -/// by newly added [WebViewPlatformController] methods. -abstract class WebViewPlatformController { - /// Creates a new WebViewPlatform. - /// - /// Callbacks made by the WebView will be delegated to `handler`. - /// - /// The `handler` parameter must not be null. - WebViewPlatformController(WebViewPlatformCallbacksHandler handler); - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, - Map? headers, - ) { - throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); - } - - /// Updates the webview settings. - /// - /// Any non null field in `settings` will be set as the new setting value. - /// All null fields in `settings` are ignored. - Future updateSettings(WebSettings setting) { - throw UnimplementedError( - "WebView updateSettings is not implemented on the current platform"); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If no URL was ever loaded, returns `null`. - Future currentUrl() { - throw UnimplementedError( - "WebView currentUrl is not implemented on the current platform"); - } - - /// Checks whether there's a back history item. - Future canGoBack() { - throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); - } - - /// Checks whether there's a forward history item. - Future canGoForward() { - throw UnimplementedError( - "WebView canGoForward is not implemented on the current platform"); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - throw UnimplementedError( - "WebView goBack is not implemented on the current platform"); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - throw UnimplementedError( - "WebView goForward is not implemented on the current platform"); - } - - /// Reloads the current URL. - Future reload() { - throw UnimplementedError( - "WebView reload is not implemented on the current platform"); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - Future clearCache() { - throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { - throw UnimplementedError( - "WebView evaluateJavascript is not implemented on the current platform"); - } - - /// Adds new JavaScript channels to the set of enabled channels. - /// - /// For each value in this list the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - /// - /// See also: [CreationParams.javascriptChannelNames]. - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); - } - - /// Removes JavaScript channel names from the set of enabled channels. - /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through - /// [CreationParams.javascriptChannelNames]. - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - throw UnimplementedError( - "WebView getTitle is not implemented on the current platform"); - } - - /// Set the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. - Future scrollTo(int x, int y) { - throw UnimplementedError( - "WebView scrollTo is not implemented on the current platform"); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. - Future scrollBy(int x, int y) { - throw UnimplementedError( - "WebView scrollBy is not implemented on the current platform"); - } - - /// Return the horizontal scroll position of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); - } - - /// Return the vertical scroll position of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); - } -} - -/// A single setting for configuring a WebViewPlatform which may be absent. -class WebSetting { - /// Constructs an absent setting instance. - /// - /// The [isPresent] field for the instance will be false. - /// - /// Accessing [value] for an absent instance will throw. - WebSetting.absent() - : _value = null, - isPresent = false; - - /// Constructs a setting of the given `value`. - /// - /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) - : _value = value, - isPresent = true; - - final T? _value; - - /// The setting's value. - /// - /// Throws if [WebSetting.isPresent] is false. - T get value { - if (!isPresent) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - assert(isPresent); - // The intention of this getter is to return T whether it is nullable or - // not whereas _value is of type T? since _value can be null even when - // T is not nullable (when isPresent == false). - // - // We promote _value to T using `as T` instead of `!` operator to handle - // the case when _value is legitimately null (and T is a nullable type). - // `!` operator would always throw if _value is null. - return _value as T; - } - - /// True when this web setting instance contains a value. - /// - /// When false the [WebSetting.value] getter throws. - final bool isPresent; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - final WebSetting typedOther = other as WebSetting; - return typedOther.isPresent == isPresent && typedOther._value == _value; - } - - @override - int get hashCode => hashValues(_value, isPresent); -} - -/// Settings for configuring a WebViewPlatform. -/// -/// Initial settings are passed as part of [CreationParams], settings updates are sent with -/// [WebViewPlatform#updateSettings]. -/// -/// The `userAgent` parameter must not be null. -class WebSettings { - /// Construct an instance with initial settings. Future setting changes can be - /// sent with [WebviewPlatform#updateSettings]. - /// - /// The `userAgent` parameter must not be null. - WebSettings({ - this.javascriptMode, - this.hasNavigationDelegate, - this.hasProgressTracking, - this.debuggingEnabled, - this.gestureNavigationEnabled, - this.allowsInlineMediaPlayback, - required this.userAgent, - }) : assert(userAgent != null); - - /// The JavaScript execution mode to be used by the webview. - final JavascriptMode? javascriptMode; - - /// Whether the [WebView] has a [NavigationDelegate] set. - final bool? hasNavigationDelegate; - - /// Whether the [WebView] should track page loading progress. - /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. - final bool? hasProgressTracking; - - /// Whether to enable the platform's webview content debugging tools. - /// - /// See also: [WebView.debuggingEnabled]. - final bool? debuggingEnabled; - - /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. - /// - /// This will have no effect on Android. - final bool? allowsInlineMediaPlayback; - - /// The value used for the HTTP `User-Agent:` request header. - /// - /// If [userAgent.value] is null the platform's default user agent should be used. - /// - /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the - /// last time it was set. - /// - /// See also [WebView.userAgent]. - final WebSetting userAgent; - - /// Whether to allow swipe based navigation in iOS. - /// - /// See also: [WebView.gestureNavigationEnabled] - final bool? gestureNavigationEnabled; - - @override - String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; - } -} - -/// Configuration to use when creating a new [WebViewPlatformController]. -/// -/// The `autoMediaPlaybackPolicy` parameter must not be null. -class CreationParams { - /// Constructs an instance to use when creating a new - /// [WebViewPlatformController]. - /// - /// The `autoMediaPlaybackPolicy` parameter must not be null. - CreationParams({ - this.initialUrl, - this.webSettings, - this.javascriptChannelNames = const {}, - this.userAgent, - this.autoMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(autoMediaPlaybackPolicy != null); - - /// The initialUrl to load in the webview. - /// - /// When null the webview will be created without loading any page. - final String? initialUrl; - - /// The initial [WebSettings] for the new webview. - /// - /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings? webSettings; - - /// The initial set of JavaScript channels that are configured for this webview. - /// - /// For each value in this set the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - // TODO(amirh): describe what should happen when postMessage is called once that code is migrated - // to PlatformWebView. - final Set javascriptChannelNames; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; - - @override - String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; - } -} - -/// Signature for callbacks reporting that a [WebViewPlatformController] was created. -/// -/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. -typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController? webViewPlatformController); - -/// Interface for a platform implementation of a WebView. -/// -/// [WebView.platform] controls the builder that is used by [WebView]. -/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations -/// for Android and iOS respectively. -abstract class WebViewPlatform { - /// Builds a new WebView. - /// - /// Returns a Widget tree that embeds the created webview. - /// - /// `creationParams` are the initial parameters used to setup the webview. - /// - /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created - /// [WebViewPlatformController]. - /// - /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] - /// implementation is created with the [WebViewPlatformController] instance as a parameter. - /// - /// `gestureRecognizers` specifies which gestures should be consumed by the web view. - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - /// - /// `webViewPlatformHandler` must not be null. - Widget build({ - required BuildContext context, - // TODO(amirh): convert this to be the actual parameters. - // I'm starting without it as the PR is starting to become pretty big. - // I'll followup with the conversion PR. - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }); - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() { - throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); - } -} +/// Re-export the classes from the webview_flutter_platform_interface through +/// the `platform_interface.dart` file so we don't accidentally break any +/// non-endorsed existing implementations of the interface. +export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + show + AutoMediaPlaybackPolicy, + CreationParams, + JavascriptChannel, + JavascriptChannelRegistry, + JavascriptMessage, + JavascriptMode, + JavascriptMessageHandler, + WebViewPlatform, + WebViewPlatformCallbacksHandler, + WebViewPlatformController, + WebViewPlatformCreatedCallback, + WebSetting, + WebSettings, + WebResourceError, + WebResourceErrorType; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart new file mode 100644 index 000000000000..7699cc46c5d3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -0,0 +1,681 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import '../platform_interface.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + static WebViewPlatform? _platform; + + /// Sets a custom [WebViewPlatform]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platform` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static set platform(WebViewPlatform? platform) { + _platform = platform; + } + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static WebViewPlatform get platform { + if (_platform == null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platform = AndroidWebView(); + break; + case TargetPlatform.iOS: + _platform = CupertinoWebView(); + break; + default: + throw UnsupportedError( + "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); + } + } + return _platform!; + } + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + + late JavascriptChannelRegistry _javascriptChannelRegistry; + late _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: _onWebViewPlatformCreated, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + javascriptChannelRegistry: _javascriptChannelRegistry, + gestureRecognizers: widget.gestureRecognizers, + creationParams: _creationParamsfromWidget(widget), + ); + } + + @override + void initState() { + super.initState(); + _assertJavascriptChannelNamesAreUnique(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _assertJavascriptChannelNamesAreUnique(); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); + } + + void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { + final WebViewController controller = WebViewController._( + widget, + webViewPlatform!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + } + + void _assertJavascriptChannelNamesAreUnique() { + if (widget.javascriptChannels == null || + widget.javascriptChannels!.isEmpty) { + return; + } + assert(_extractChannelNames(widget.javascriptChannels).length == + widget.javascriptChannels!.length); + } +} + +CreationParams _creationParamsfromWidget(WebView widget) { + return CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), + userAgent: widget.userAgent, + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + ); +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} + +// This method assumes that no fields in `currentValue` are null. +WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); +} + +Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget); + + WebView _widget; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + final NavigationRequest request = + NavigationRequest._(url: url, isForMainFrame: isForMainFrame); + final bool allowNavigation = _widget.navigationDelegate == null || + await _widget.navigationDelegate!(request) == + NavigationDecision.navigate; + return allowNavigation; + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_widget.onWebResourceError != null) { + _widget.onWebResourceError!(error); + } + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final WebViewPlatformController _webViewPlatformController; + final JavascriptChannelRegistry _javascriptChannelRegistry; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels(widget.javascriptChannels); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } +} + +/// Manages cookies pertaining to all [WebView]s. +class CookieManager { + /// Creates a [CookieManager] -- returns the instance if it's already been called. + factory CookieManager() { + return _instance ??= CookieManager._(); + } + + CookieManager._(); + + static CookieManager? _instance; + + /// Clears all cookies for all [WebView] instances. + /// + /// This is a no op on iOS version smaller than 9. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() => WebView.platform.clearCookies(); +} + +// Throws an ArgumentError if `url` is not a valid URL string. +void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart deleted file mode 100644 index ca1440d69929..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an Android webview. -/// -/// This is used as the default implementation for [WebView.platform] on Android. It uses -/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class AndroidWebView implements WebViewPlatform { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart deleted file mode 100644 index 8d4be3800a28..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an iOS webview. -/// -/// This is used as the default implementation for [WebView.platform] on iOS. It uses -/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class CupertinoWebView implements WebViewPlatform { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart deleted file mode 100644 index 05831a9d8794..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -import '../platform_interface.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : assert(_platformCallbacksHandler != null), - _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']!; - final String message = call.arguments['message']!; - _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url']!, - isForMainFrame: call.arguments['isForMainFrame']!, - ); - case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']!); - return null; - case 'onProgress': - _platformCallbacksHandler.onProgress(call.arguments['progress']); - return null; - case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']!); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode']!, - description: call.arguments['description']!, - // iOS doesn't support `failingUrl`. - failingUrl: call.arguments['failingUrl'], - domain: call.arguments['domain'], - errorType: call.arguments['errorType'] == null - ? null - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadUrl( - String url, - Map? headers, - ) async { - assert(url != null); - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => - _channel.invokeMethod("canGoBack").then((result) => result!); - - @override - Future canGoForward() => - _channel.invokeMethod("canGoForward").then((result) => result!); - - @override - Future goBack() => _channel.invokeMethod("goBack"); - - @override - Future goForward() => _channel.invokeMethod("goForward"); - - @override - Future reload() => _channel.invokeMethod("reload"); - - @override - Future clearCache() => _channel.invokeMethod("clearCache"); - - @override - Future updateSettings(WebSettings settings) async { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isNotEmpty) { - await _channel.invokeMethod('updateSettings', updatesMap); - } - } - - @override - Future evaluateJavascript(String javascriptString) { - return _channel - .invokeMethod('evaluateJavascript', javascriptString) - .then((result) => result!); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod("getTitle"); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => - _channel.invokeMethod("getScrollX").then((result) => result!); - - @override - Future getScrollY() => - _channel.invokeMethod("getScrollY").then((result) => result!); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result!); - } - - static Map _webSettingsToMap(WebSettings? settings) { - final Map map = {}; - void _addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - void _addSettingIfPresent(String key, WebSetting setting) { - if (!setting.isPresent) { - return; - } - map[key] = setting.value; - } - - _addIfNonNull('jsMode', settings!.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); - _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); - _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); - _addIfNonNull( - 'gestureNavigationEnabled', settings.gestureNavigationEnabled); - _addIfNonNull( - 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); - _addSettingIfPresent('userAgent', settings.userAgent); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams, { - bool usesHybridComposition = false, - }) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - 'usesHybridComposition': usesHybridComposition, - }; - } -} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 398ac876bf3e..ba38771e5107 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -2,833 +2,9 @@ // 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'; +export 'package:webview_flutter_android/webview_android.dart'; +export 'package:webview_flutter_android/webview_surface_android.dart'; +export 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'platform_interface.dart'; -import 'src/webview_android.dart'; -import 'src/webview_cupertino.dart'; -import 'src/webview_method_channel.dart'; - -/// Optional callback invoked when a web view is first created. [controller] is -/// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); - -/// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { - /// JavaScript execution is disabled. - disabled, - - /// JavaScript execution is not restricted. - unrestricted, -} - -/// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} - -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest._({required this.url, required this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. -/// -/// To use this, set [WebView.platform] to an instance of this class. -/// -/// This implementation uses hybrid composition to render the [WebView] on -/// Android. It solves multiple issues related to accessibility and interaction -/// with the [WebView] at the cost of some performance on Android versions below -/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more -/// information. -class SurfaceAndroidWebView extends AndroidWebView { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - }) { - assert(Platform.isAndroid); - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/webview', - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: MethodChannelWebViewPlatform.creationParamsToMap( - creationParams, - usesHybridComposition: true, - ), - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler), - ); - }) - ..create(); - }, - ); - } -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); - -/// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); - -/// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); - -/// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); - -/// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); - -/// Specifies possible restrictions on automatic media playback. -/// -/// This is typically used in [WebView.initialMediaPlaybackPolicy]. -// The method channel implementation is marshalling this enum to the value's index, so the order -// is important. -enum AutoMediaPlaybackPolicy { - /// Starting any kind of media playback requires a user action. - /// - /// For example: JavaScript code cannot start playing media unless the code was executed - /// as a result of a user action (like a touch event). - require_user_action_for_all_media_types, - - /// Starting any kind of media playback is always allowed. - /// - /// For example: JavaScript code that's triggered when the page is loaded can start playing - /// video or audio. - always_allow, -} - -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); - -/// A named channel for receiving messaged from JavaScript code running inside a web view. -class JavascriptChannel { - /// Constructs a Javascript channel. - /// - /// The parameters `name` and `onMessageReceived` must not be null. - JavascriptChannel({ - required this.name, - required this.onMessageReceived, - }) : assert(name != null), - assert(onMessageReceived != null), - assert(_validChannelNames.hasMatch(name)); - - /// The channel's name. - /// - /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. - /// - /// The name must start with a letter or underscore(_), followed by any combination of those - /// characters plus digits. - /// - /// Note that any JavaScript existing `window` property with this name will be overriden. - /// - /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. - final String name; - - /// A callback that's invoked when a message is received through the channel. - final JavascriptMessageHandler onMessageReceived; -} - -/// A web view widget for showing html content. -/// -/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering -/// the `WebView` is not able to block the `WebView` from receiving touch events. -/// See https://github.com/flutter/flutter/issues/53490. -class WebView extends StatefulWidget { - /// Creates a new web view. - /// - /// The web view can be controlled using a `WebViewController` that is passed to the - /// `onWebViewCreated` callback once the web view is created. - /// - /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. - const WebView({ - Key? key, - this.onWebViewCreated, - this.initialUrl, - this.javascriptMode = JavascriptMode.disabled, - this.javascriptChannels, - this.navigationDelegate, - this.gestureRecognizers, - this.onPageStarted, - this.onPageFinished, - this.onProgress, - this.onWebResourceError, - this.debuggingEnabled = false, - this.gestureNavigationEnabled = false, - this.userAgent, - this.initialMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - this.allowsInlineMediaPlayback = false, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null), - super(key: key); - - static WebViewPlatform? _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform? platform) { - _platform = platform; - } - - /// The WebView platform that's used by this WebView. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static WebViewPlatform get platform { - if (_platform == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - _platform = AndroidWebView(); - break; - case TargetPlatform.iOS: - _platform = CupertinoWebView(); - break; - default: - throw UnsupportedError( - "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); - } - } - return _platform!; - } - - /// If not null invoked once the web view is created. - final WebViewCreatedCallback? onWebViewCreated; - - /// Which gestures should be consumed by the web view. - /// - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// - /// When this set is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - final Set>? gestureRecognizers; - - /// The initial URL to load. - final String? initialUrl; - - /// Whether Javascript execution is enabled. - final JavascriptMode javascriptMode; - - /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. - /// - /// For each [JavascriptChannel] in the set, a channel object is made available for the - /// JavaScript code in a window property named [JavascriptChannel.name]. - /// The JavaScript code can then call `postMessage` on that object to send a message that will be - /// passed to [JavascriptChannel.onMessageReceived]. - /// - /// For example for the following JavascriptChannel: - /// - /// ```dart - /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); - /// ``` - /// - /// JavaScript code can call: - /// - /// ```javascript - /// Print.postMessage('Hello'); - /// ``` - /// - /// To asynchronously invoke the message handler which will print the message to standard output. - /// - /// Adding a new JavaScript channel only takes affect after the next page is loaded. - /// - /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple - /// channels in the list. - /// - /// A null value is equivalent to an empty set. - final Set? javascriptChannels; - - /// A delegate function that decides how to handle navigation actions. - /// - /// When a navigation is initiated by the WebView (e.g when a user clicks a link) - /// this delegate is called and has to decide how to proceed with the navigation. - /// - /// See [NavigationDecision] for possible decisions the delegate can take. - /// - /// When null all navigation actions are allowed. - /// - /// Caveats on Android: - /// - /// * Navigation actions targeted to the main frame can be intercepted, - /// navigation actions targeted to subframes are allowed regardless of the value - /// returned by this delegate. - /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were - /// triggered by a user gesture, this disables some of Chromium's security mechanisms. - /// A navigationDelegate should only be set when loading trusted content. - /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have - /// a later version): - /// * When a navigationDelegate is set pages with frames are not properly handled by the - /// webview, and frames will be opened in the main frame. - /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate? navigationDelegate; - - /// Controls whether inline playback of HTML5 videos is allowed on iOS. - /// - /// This field is ignored on Android because Android allows it by default. - /// - /// By default `allowsInlineMediaPlayback` is false. - final bool allowsInlineMediaPlayback; - - /// Invoked when a page starts loading. - final PageStartedCallback? onPageStarted; - - /// Invoked when a page has finished loading. - /// - /// This is invoked only for the main frame. - /// - /// When [onPageFinished] is invoked on Android, the page being rendered may - /// not be updated yet. - /// - /// When invoked on iOS or Android, any Javascript code that is embedded - /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback? onPageFinished; - - /// Invoked when a page is loading. - final PageLoadingCallback? onProgress; - - /// Invoked when a web resource has failed to load. - /// - /// This callback is only called for the main page. - final WebResourceErrorCallback? onWebResourceError; - - /// Controls whether WebView debugging is enabled. - /// - /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). - /// - /// WebView debugging is enabled by default in dev builds on iOS. - /// - /// To debug WebViews on iOS: - /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) - /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> - /// - /// By default `debuggingEnabled` is false. - final bool debuggingEnabled; - - /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. - /// - /// This only works on iOS. - /// - /// By default `gestureNavigationEnabled` is false. - final bool gestureNavigationEnabled; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - /// - /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. - /// - /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. - /// - /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom - /// user agent. - /// - /// By default `userAgent` is null. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - /// - /// This initial value is applied to the platform's webview upon creation. Any following - /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). - /// - /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. - final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; - - @override - State createState() => _WebViewState(); -} - -class _WebViewState extends State { - final Completer _controller = - Completer(); - - late _PlatformCallbacksHandler _platformCallbacksHandler; - - @override - Widget build(BuildContext context) { - return WebView.platform.build( - context: context, - onWebViewPlatformCreated: _onWebViewPlatformCreated, - webViewPlatformCallbacksHandler: _platformCallbacksHandler, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _creationParamsfromWidget(widget), - ); - } - - @override - void initState() { - super.initState(); - _assertJavascriptChannelNamesAreUnique(); - _platformCallbacksHandler = _PlatformCallbacksHandler(widget); - } - - @override - void didUpdateWidget(WebView oldWidget) { - super.didUpdateWidget(oldWidget); - _assertJavascriptChannelNamesAreUnique(); - _controller.future.then((WebViewController controller) { - _platformCallbacksHandler._widget = widget; - controller._updateWidget(widget); - }); - } - - void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { - final WebViewController controller = WebViewController._( - widget, webViewPlatform!, _platformCallbacksHandler); - _controller.complete(controller); - if (widget.onWebViewCreated != null) { - widget.onWebViewCreated!(controller); - } - } - - void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels!.isEmpty) { - return; - } - assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels!.length); - } -} - -CreationParams _creationParamsfromWidget(WebView widget) { - return CreationParams( - initialUrl: widget.initialUrl, - webSettings: _webSettingsFromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), - userAgent: widget.userAgent, - autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, - ); -} - -WebSettings _webSettingsFromWidget(WebView widget) { - return WebSettings( - javascriptMode: widget.javascriptMode, - hasNavigationDelegate: widget.navigationDelegate != null, - hasProgressTracking: widget.onProgress != null, - debuggingEnabled: widget.debuggingEnabled, - gestureNavigationEnabled: widget.gestureNavigationEnabled, - allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, - userAgent: WebSetting.of(widget.userAgent), - ); -} - -// This method assumes that no fields in `currentValue` are null. -WebSettings _clearUnchangedWebSettings( - WebSettings currentValue, WebSettings newValue) { - assert(currentValue.javascriptMode != null); - assert(currentValue.hasNavigationDelegate != null); - assert(currentValue.hasProgressTracking != null); - assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); - assert(newValue.javascriptMode != null); - assert(newValue.hasNavigationDelegate != null); - assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); - - JavascriptMode? javascriptMode; - bool? hasNavigationDelegate; - bool? hasProgressTracking; - bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); - if (currentValue.javascriptMode != newValue.javascriptMode) { - javascriptMode = newValue.javascriptMode; - } - if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { - hasNavigationDelegate = newValue.hasNavigationDelegate; - } - if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { - hasProgressTracking = newValue.hasProgressTracking; - } - if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { - debuggingEnabled = newValue.debuggingEnabled; - } - if (currentValue.userAgent != newValue.userAgent) { - userAgent = newValue.userAgent; - } - - return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - hasProgressTracking: hasProgressTracking, - debuggingEnabled: debuggingEnabled, - userAgent: userAgent, - ); -} - -Set _extractChannelNames(Set? channels) { - final Set channelNames = channels == null - ? {} - : channels.map((JavascriptChannel channel) => channel.name).toSet(); - return channelNames; -} - -class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { - _PlatformCallbacksHandler(this._widget) { - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - } - - WebView _widget; - - // Maps a channel name to a channel. - final Map _javascriptChannels = - {}; - - @override - void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel]!.onMessageReceived(JavascriptMessage(message)); - } - - @override - FutureOr onNavigationRequest({ - required String url, - required bool isForMainFrame, - }) async { - final NavigationRequest request = - NavigationRequest._(url: url, isForMainFrame: isForMainFrame); - final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate!(request) == - NavigationDecision.navigate; - return allowNavigation; - } - - @override - void onPageStarted(String url) { - if (_widget.onPageStarted != null) { - _widget.onPageStarted!(url); - } - } - - @override - void onPageFinished(String url) { - if (_widget.onPageFinished != null) { - _widget.onPageFinished!(url); - } - } - - @override - void onProgress(int progress) { - if (_widget.onProgress != null) { - _widget.onProgress!(progress); - } - } - - void onWebResourceError(WebResourceError error) { - if (_widget.onWebResourceError != null) { - _widget.onWebResourceError!(error); - } - } - - void _updateJavascriptChannelsFromSet(Set? channels) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; - } - } -} - -/// Controls a [WebView]. -/// -/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] -/// callback for a [WebView] widget. -class WebViewController { - WebViewController._( - this._widget, - this._webViewPlatformController, - this._platformCallbacksHandler, - ) : assert(_webViewPlatformController != null) { - _settings = _webSettingsFromWidget(_widget); - } - - final WebViewPlatformController _webViewPlatformController; - - final _PlatformCallbacksHandler _platformCallbacksHandler; - - late WebSettings _settings; - - WebView _widget; - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, { - Map? headers, - }) async { - assert(url != null); - _validateUrlString(url); - return _webViewPlatformController.loadUrl(url, headers); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If [WebView.initialUrl] was never specified, returns `null`. - /// Note that this operation is asynchronous, and it is possible that the - /// current URL changes again by the time this function returns (in other - /// words, by the time this future completes, the WebView may be displaying a - /// different URL). - Future currentUrl() { - return _webViewPlatformController.currentUrl(); - } - - /// Checks whether there's a back history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has - /// changed by the time the future completed. - Future canGoBack() { - return _webViewPlatformController.canGoBack(); - } - - /// Checks whether there's a forward history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has - /// changed by the time the future completed. - Future canGoForward() { - return _webViewPlatformController.canGoForward(); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - return _webViewPlatformController.goBack(); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - return _webViewPlatformController.goForward(); - } - - /// Reloads the current URL. - Future reload() { - return _webViewPlatformController.reload(); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - /// - /// Note: Calling this method also triggers a reload. - Future clearCache() async { - await _webViewPlatformController.clearCache(); - return reload(); - } - - Future _updateWidget(WebView widget) async { - _widget = widget; - await _updateSettings(_webSettingsFromWidget(widget)); - await _updateJavascriptChannels(widget.javascriptChannels); - } - - Future _updateSettings(WebSettings newSettings) { - final WebSettings update = - _clearUnchangedWebSettings(_settings, newSettings); - _settings = newSettings; - return _webViewPlatformController.updateSettings(update); - } - - Future _updateJavascriptChannels( - Set? newChannels) async { - final Set currentChannels = - _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); - if (channelsToRemove.isNotEmpty) { - await _webViewPlatformController - .removeJavascriptChannels(channelsToRemove); - } - if (channelsToAdd.isNotEmpty) { - await _webViewPlatformController.addJavascriptChannels(channelsToAdd); - } - _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: - /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. - /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. - /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript - /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { - if (_settings.javascriptMode == JavascriptMode.disabled) { - return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); - } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - return _webViewPlatformController.getTitle(); - } - - /// Sets the WebView's content scroll position. - /// - /// The parameters `x` and `y` specify the scroll position in WebView pixels. - Future scrollTo(int x, int y) { - return _webViewPlatformController.scrollTo(x, y); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. - Future scrollBy(int x, int y) { - return _webViewPlatformController.scrollBy(x, y); - } - - /// Return the horizontal scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - return _webViewPlatformController.getScrollX(); - } - - /// Return the vertical scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - return _webViewPlatformController.getScrollY(); - } -} - -/// Manages cookies pertaining to all [WebView]s. -class CookieManager { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManager() { - return _instance ??= CookieManager._(); - } - - CookieManager._(); - - static CookieManager? _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// This is a no op on iOS version smaller than 9. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); -} - -// Throws an ArgumentError if `url` is not a valid URL string. -void _validateUrlString(String url) { - try { - final Uri uri = Uri.parse(url); - if (uri.scheme.isEmpty) { - throw ArgumentError('Missing scheme in URL string: "$url"'); - } - } on FormatException catch (e) { - throw ArgumentError(e); - } -} +export 'platform_interface.dart'; +export 'src/webview.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 393a66e3f92e..985f3178916f 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -12,14 +12,16 @@ flutter: plugin: platforms: android: - package: io.flutter.plugins.webviewflutter - pluginClass: WebViewFlutterPlugin + default_package: webview_flutter_android ios: - pluginClass: FLTWebViewFlutterPlugin + default_package: webview_flutter_wkwebview dependencies: flutter: sdk: flutter + webview_flutter_platform_interface: ^1.0.0 + webview_flutter_android: ^2.0.13 + webview_flutter_wkwebview: ^2.0.13 dev_dependencies: flutter_driver: diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index 5efee6d9952d..f7d09266a64b 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -10,7 +10,7 @@ import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter/webview_flutter.dart'; typedef void VoidCallback(); @@ -1175,6 +1175,7 @@ class MyWebViewPlatform implements WebViewPlatform { BuildContext? context, CreationParams? creationParams, required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, WebViewPlatformCreatedCallback? onWebViewPlatformCreated, Set>? gestureRecognizers, }) { From 8c5f0f04cade805176358db125ba9fc1b2b25fc3 Mon Sep 17 00:00:00 2001 From: Baw Appie Date: Fri, 24 Sep 2021 00:28:12 +0900 Subject: [PATCH 067/259] [url_launcher] Error handling when URL cannot be parsed with Uri.parse (#4365) --- .../url_launcher/url_launcher/CHANGELOG.md | 4 +++ .../url_launcher/lib/url_launcher.dart | 6 ++-- .../url_launcher/url_launcher/pubspec.yaml | 2 +- .../url_launcher/test/url_launcher_test.dart | 36 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 4b52a8d46f8b..fff325e08915 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.12 + +* Fixed an error where 'launch' method of url_launcher would cause an error if the provided URL was not valid by RFC 3986. + ## 6.0.11 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index 239e3c46c480..300f96f4a179 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -71,8 +71,10 @@ Future launch( Brightness? statusBarBrightness, String? webOnlyWindowName, }) async { - final Uri url = Uri.parse(urlString.trimLeft()); - final bool isWebURL = url.scheme == 'http' || url.scheme == 'https'; + final Uri? url = Uri.tryParse(urlString.trimLeft()); + final bool isWebURL = + url != null && (url.scheme == 'http' || url.scheme == 'https'); + if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { throw PlatformException( code: 'NOT_A_WEB_SCHEME', diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index c90d2feb08f4..8edb9e21c535 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/master/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.11 +version: 6.0.12 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index 04f727a57746..a038746d6bec 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -281,6 +281,42 @@ void main() { await launchResult; expect(binding.renderView.automaticSystemUiAdjustment, true); }); + + test('open non-parseable url', () async { + mock + ..setLaunchExpectations( + url: + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1'), + isTrue); + }); + + test('cannot open non-parseable url with forceSafariVC: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceSafariVC: true), + throwsA(isA())); + }); + + test('cannot open non-parseable url with forceWebView: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceWebView: true), + throwsA(isA())); + }); }); } From cdabfa9cf7a16fb439667d202e3b5fe9d99036b0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Sep 2021 13:23:06 -0400 Subject: [PATCH 068/259] [flutter_plugin_tools] Allow overriding breaking change check (#4369) --- .cirrus.yml | 16 +++- script/tool/CHANGELOG.md | 2 + .../lib/src/common/repository_package.dart | 5 + .../tool/lib/src/drive_examples_command.dart | 2 +- .../src/federation_safety_check_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 83 +++++++++++++++-- .../tool/test/version_check_command_test.dart | 91 ++++++++++++++++++- 7 files changed, 188 insertions(+), 13 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 8dcb4d96d2be..67343ee15f88 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -68,7 +68,21 @@ task: - cd script/tool - dart pub run test - name: publishable - version_check_script: ./script/tool_runner.sh version-check + env: + CHANGE_DESC: "$TMPDIR/change-description.txt" + version_check_script: + # For pre-submit, pass the PR description to the script to allow for + # platform version breaking version change justifications. + # For post-submit, ignore platform version breaking version changes. + # The PR description isn't reliably part of the commit message, so using + # the same flags as for presubmit would likely result in false-positive + # post-submit failures. + - if [[ $CIRRUS_PR == "" ]]; then + - ./script/tool_runner.sh version-check --ignore-platform-interface-breaks + - else + - echo "$CIRRUS_CHANGE_MESSAGE" > "$CHANGE_DESC" + - ./script/tool_runner.sh version-check --change-description-file="$CHANGE_DESC" + - fi publish_check_script: ./script/tool_runner.sh publish-check - name: format format_script: ./script/tool_runner.sh format --fail-on-change diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7e9cd3bec938..2bc7a901a9a2 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -6,6 +6,8 @@ federated packages that have been done in such a way that they will pass in CI, but fail once the change is landed and published. - `publish-check` now validates that there is an `AUTHORS` file. +- Added flags to `version-check` to allow overriding the platform interface + major version change restriction. ## 0.7.1 diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index feece7c1cdff..cb586afb4dfe 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -53,6 +53,11 @@ class RepositoryPackage { directory.parent.basename != 'packages' && directory.basename.startsWith(directory.parent.basename); + /// True if this appears to be a platform interface package, according to + /// repository conventions. + bool get isPlatformInterface => + directory.basename.endsWith('_platform_interface'); + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index b3434b0659f3..593e557fa395 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -133,7 +133,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - if (package.directory.basename.endsWith('_platform_interface') && + if (package.isPlatformInterface && !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index fd53d6cbaa67..200f9c3f48cb 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -109,7 +109,7 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { return PackageResult.skip('Not a federated plugin.'); } - if (package.directory.basename.endsWith('_platform_interface')) { + if (package.isPlatformInterface) { // As the leaf nodes in the graph, a published package interface change is // assumed to be correct, and other changes are validated against that. return PackageResult.skip( diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 6b49c40d66bb..528251fbf80d 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -18,6 +18,8 @@ import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; +const int _exitMissingChangeDescriptionFile = 3; + /// Categories of version change types. enum NextVersionType { /// A breaking change. @@ -108,13 +110,36 @@ class VersionCheckCommand extends PackageLoopingCommand { argParser.addFlag( _againstPubFlag, help: 'Whether the version check should run against the version on pub.\n' - 'Defaults to false, which means the version check only run against the previous version in code.', + 'Defaults to false, which means the version check only run against ' + 'the previous version in code.', defaultsTo: false, negatable: true, ); + argParser.addOption(_changeDescriptionFile, + help: 'The path to a file containing the description of the change ' + '(e.g., PR description or commit message).\n\n' + 'If supplied, this is used to allow overrides to some version ' + 'checks.'); + argParser.addFlag(_ignorePlatformInterfaceBreaks, + help: 'Bypasses the check that platform interfaces do not contain ' + 'breaking changes.\n\n' + 'This is only intended for use in post-submit CI checks, to ' + 'prevent the possibility of post-submit breakage if a change ' + 'description justification is not transferred into the commit ' + 'message. Pre-submit checks should always use ' + '--$_changeDescriptionFile instead.', + hide: true); } static const String _againstPubFlag = 'against-pub'; + static const String _changeDescriptionFile = 'change-description-file'; + static const String _ignorePlatformInterfaceBreaks = + 'ignore-platform-interface-breaks'; + + /// The string that must be in [_changeDescriptionFile] to allow a breaking + /// change to a platform interface. + static const String _breakingChangeJustificationMarker = + '## Breaking change justification'; final PubVersionFinder _pubVersionFinder; @@ -292,16 +317,17 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - final bool isPlatformInterface = - pubspec.name.endsWith('_platform_interface'); - // TODO(stuartmorgan): Relax this check. See - // https://github.com/flutter/flutter/issues/85391 - if (isPlatformInterface && - allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { + if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR && + !_validateBreakingChange(package)) { printError('${indentation}Breaking change detected.\n' - '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); + '${indentation}Breaking changes to platform interfaces are not ' + 'allowed without explicit justification.\n' + '${indentation}See ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'); return _CurrentVersionState.invalidChange; } + return _CurrentVersionState.validChange; } @@ -398,4 +424,45 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return null; } } + + /// Checks whether the current breaking change to [package] should be allowed, + /// logging extra information for auditing when allowing unusual cases. + bool _validateBreakingChange(RepositoryPackage package) { + // Only platform interfaces have breaking change restrictions. + if (!package.isPlatformInterface) { + return true; + } + + if (getBoolArg(_ignorePlatformInterfaceBreaks)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to --$_ignorePlatformInterfaceBreaks'); + return true; + } + + if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to "$_breakingChangeJustificationMarker" in the change ' + 'description.'); + return true; + } + + return false; + } + + /// Returns the contents of the file pointed to by [_changeDescriptionFile], + /// or an empty string if that flag is not provided. + String _getChangeDescription() { + final String path = getStringArg(_changeDescriptionFile); + if (path.isEmpty) { + return ''; + } + final File file = packagesDir.fileSystem.file(path); + if (!file.existsSync()) { + printError('${indentation}No such file: $path'); + throw ToolExit(_exitMissingChangeDescriptionFile); + } + return file.readAsStringSync(); + } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 9ab7c57089a3..7d59dbb3ee7e 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -46,7 +46,7 @@ class MockProcessResult extends Mock implements io.ProcessResult {} void main() { const String indentation = ' '; group('$VersionCheckCommand', () { - FileSystem fileSystem; + late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; @@ -252,7 +252,8 @@ void main() { ])); }); - test('disallows breaking changes to platform interfaces', () async { + test('disallows breaking changes to platform interfaces by default', + () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -276,6 +277,92 @@ void main() { ])); }); + test('allows breaking changes to platform interfaces with explanation', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync(''' +Some general PR description + +## Breaking change justification + +This is necessary because of X, Y, and Z + +## Another section'''); + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface ' + 'due to "## Breaking change justification" in the change ' + 'description.'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + + test('throws if a nonexistent change description file is specified', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=a_missing_file.txt' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No such file: a_missing_file.txt'), + ]), + ); + }); + + test('allows breaking changes to platform interfaces with bypass flag', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--ignore-platform-interface-breaks' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface due ' + 'to --ignore-platform-interface-breaks'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('Allow empty lines in front of the first version in CHANGELOG', () async { const String version = '1.0.1'; From d2c6c4314412f4d8edb5aaff8d422a9c72dac10c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Sep 2021 15:40:04 -0400 Subject: [PATCH 069/259] [flutter_plugin_tools] Improve version check error handling (#4376) Catches invalid CHANGELOG formats and logs useful error messages for them, rather than throwing FormatExceptions. --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/version_check_command.dart | 23 +++++-- .../tool/test/version_check_command_test.dart | 67 +++++++++++++++++++ 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2bc7a901a9a2..a5263ba03965 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -8,6 +8,7 @@ - `publish-check` now validates that there is an `AUTHORS` file. - Added flags to `version-check` to allow overriding the platform interface major version change restriction. +- Improved error handling and error messages in CHANGELOG version checks. ## 0.7.1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 528251fbf80d..90ba0668002b 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -374,8 +374,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating - // CHANGELOG. That means the next version entry in the CHANGELOG pass the - // normal validation. + // CHANGELOG. That means the next version entry in the CHANGELOG should + // pass the normal validation. + versionString = null; while (iterator.moveNext()) { if (iterator.current.trim().startsWith('## ')) { versionString = iterator.current.trim().split(' ').last; @@ -384,11 +385,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } - final Version? fromChangeLog = - versionString == null ? null : Version.parse(versionString); - if (fromChangeLog == null) { - printError( - '${indentation}Cannot find version on the first line CHANGELOG.md'); + if (versionString == null) { + printError('${indentation}Unable to find a version in CHANGELOG.md'); + print('${indentation}The current version should be on a line starting ' + 'with "## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'); + return false; + } + + final Version fromChangeLog; + try { + fromChangeLog = Version.parse(versionString); + } on FormatException { + printError('"$versionString" could not be parsed as a version.'); return false; } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 7d59dbb3ee7e..39132212d664 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -601,6 +601,73 @@ This is necessary because of X, Y, and Z ); }); + test( + 'fails gracefully if the version headers are not found due to using the wrong style', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## NEXT +* Some changes for a later release. +# 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to find a version in CHANGELOG.md'), + contains('The current version should be on a line starting with ' + '"## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'), + ]), + ); + }); + + test('fails gracefully if the version is unparseable', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## Alpha +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"Alpha" could not be parsed as a version.'), + ]), + ); + }); + test('allows valid against pub', () async { mockHttpResponse = { 'name': 'some_package', From a5cd0c3aea707b03e191b1b6696bc9c14f180f9f Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Fri, 24 Sep 2021 13:43:55 +1200 Subject: [PATCH 070/259] [google_maps_flutter_platfomr_interface] Add Marker drag events (#2653) This PR adds onDragStart(LatLng) and onDrag(LatLng) events to Marker objects, in addition to the already existing onDragEnd. --- .../CHANGELOG.md | 4 + .../lib/src/events/map_event.dart | 20 +++ .../method_channel_google_maps_flutter.dart | 24 +++ .../google_maps_flutter_platform.dart | 10 ++ .../lib/src/types/marker.dart | 15 +- .../pubspec.yaml | 3 +- ...thod_channel_google_maps_flutter_test.dart | 54 ++++++ .../test/types/marker_test.dart | 167 ++++++++++++++++++ 8 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart 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 5d361d8e0c7c..464c33ed754e 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,7 @@ +## 2.1.2 + +* Add additional marker drag events + ## 2.1.1 * Method `buildViewWithTextDirection` has been added to the platform interface. 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 be426483193d..614cbe8e29fb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -102,6 +102,26 @@ class InfoWindowTapEvent extends MapEvent { InfoWindowTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId); } +/// An event fired when a [Marker] is starting to be dragged to a new [LatLng]. +class MarkerDragStartEvent extends _PositionedMapEvent { + /// Build a MarkerDragStart Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was picked up from. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragStartEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + +/// An event fired when a [Marker] is being dragged to a new [LatLng]. +class MarkerDragEvent extends _PositionedMapEvent { + /// Build a MarkerDrag Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was dragged to. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + /// An event fired when a [Marker] is dragged to a new [LatLng]. class MarkerDragEndEvent extends _PositionedMapEvent { /// Build a MarkerDragEnd Event triggered from the map represented by `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 2b9c71ee85bd..99f4fddaccd3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -124,6 +124,16 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); @@ -174,6 +184,20 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerId(call.arguments['markerId']), )); break; + case 'marker#onDragStart': + _mapEventStreamController.add(MarkerDragStartEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId']), + )); + break; + case 'marker#onDrag': + _mapEventStreamController.add(MarkerDragEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId']), + )); + break; case 'marker#onDragEnd': _mapEventStreamController.add(MarkerDragEndEvent( mapId, 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 2bb0ab2588f9..08b4872ad5dd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -303,6 +303,16 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('onInfoWindowTap() has not been implemented.'); } + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDragStart({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDrag({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + /// A [Marker] has been dragged to a different [LatLng] position. Stream onMarkerDragEnd({required int mapId}) { throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); 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 0d1b780c24d2..52255f84f4cc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -147,6 +147,8 @@ class Marker implements MapsObject { this.visible = true, this.zIndex = 0.0, this.onTap, + this.onDrag, + this.onDragStart, this.onDragEnd, }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); @@ -207,9 +209,15 @@ class Marker implements MapsObject { /// Callbacks to receive tap events for markers placed on this map. final VoidCallback? onTap; + /// Signature reporting the new [LatLng] at the start of a drag event. + final ValueChanged? onDragStart; + /// Signature reporting the new [LatLng] at the end of a drag event. final ValueChanged? onDragEnd; + /// Signature reporting the new [LatLng] during the drag event. + final ValueChanged? onDrag; + /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ @@ -225,6 +233,8 @@ class Marker implements MapsObject { bool? visibleParam, double? zIndexParam, VoidCallback? onTapParam, + ValueChanged? onDragStartParam, + ValueChanged? onDragParam, ValueChanged? onDragEndParam, }) { return Marker( @@ -241,6 +251,8 @@ class Marker implements MapsObject { visible: visibleParam ?? visible, zIndex: zIndexParam ?? zIndex, onTap: onTapParam ?? onTap, + onDragStart: onDragStartParam ?? onDragStart, + onDrag: onDragParam ?? onDrag, onDragEnd: onDragEndParam ?? onDragEnd, ); } @@ -300,6 +312,7 @@ class Marker implements MapsObject { return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, ' 'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, ' 'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, ' - 'visible: $visible, zIndex: $zIndex, onTap: $onTap}'; + 'visible: $visible, zIndex: $zIndex, onTap: $onTap, onDragStart: $onDragStart, ' + 'onDrag: $onDrag, onDragEnd: $onDragEnd}'; } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 1dc73f442d2e..2a2c9cfa8b46 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,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ 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.1 +version: 2.1.2 environment: sdk: '>=2.12.0 <3.0.0' @@ -19,6 +19,7 @@ dependencies: stream_transform: ^2.0.0 dev_dependencies: + async: ^2.5.0 flutter_test: sdk: flutter mockito: ^5.0.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 19e81c960839..176f702ff0ff 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,8 +5,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart'; import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'dart:async'; + +import 'package:async/async.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -33,6 +37,15 @@ void main() { }); } + Future sendPlatformMessage( + int mapId, String method, Map data) async { + final ByteData byteData = const StandardMethodCodec() + .encodeMethodCall(MethodCall(method, data)); + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + "plugins.flutter.io/google_maps_$mapId", byteData, (data) {}); + } + // Calls each method that uses invokeMethod with a return type other than // void to ensure that the casting/nullability handling succeeds. // @@ -68,5 +81,46 @@ void main() { 'map#takeSnapshot', ]); }); + test('markers send drag event to correct streams', () async { + const int mapId = 1; + final jsonMarkerDragStartEvent = { + "mapId": mapId, + "markerId": "drag-start-marker", + "position": [1.0, 1.0] + }; + final jsonMarkerDragEvent = { + "mapId": mapId, + "markerId": "drag-marker", + "position": [1.0, 1.0] + }; + final jsonMarkerDragEndEvent = { + "mapId": mapId, + "markerId": "drag-end-marker", + "position": [1.0, 1.0] + }; + + final MethodChannelGoogleMapsFlutter maps = + MethodChannelGoogleMapsFlutter(); + maps.ensureChannelInitialized(mapId); + + final StreamQueue markerDragStartStream = + StreamQueue(maps.onMarkerDragStart(mapId: mapId)); + final StreamQueue markerDragStream = + StreamQueue(maps.onMarkerDrag(mapId: mapId)); + final StreamQueue markerDragEndStream = + StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); + + await sendPlatformMessage( + mapId, "marker#onDragStart", jsonMarkerDragStartEvent); + await sendPlatformMessage(mapId, "marker#onDrag", jsonMarkerDragEvent); + await sendPlatformMessage( + mapId, "marker#onDragEnd", jsonMarkerDragEndEvent); + + expect((await markerDragStartStream.next).value.value, + equals("drag-start-marker")); + expect((await markerDragStream.next).value.value, equals("drag-marker")); + expect((await markerDragEndStream.next).value.value, + equals("drag-end-marker")); + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart new file mode 100644 index 000000000000..c8f6fa527a95 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart @@ -0,0 +1,167 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$Marker', () { + test('constructor defaults', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + + expect(marker.alpha, equals(1.0)); + expect(marker.anchor, equals(const Offset(0.5, 1.0))); + expect(marker.consumeTapEvents, equals(false)); + expect(marker.draggable, equals(false)); + expect(marker.flat, equals(false)); + expect(marker.icon, equals(BitmapDescriptor.defaultMarker)); + expect(marker.infoWindow, equals(InfoWindow.noText)); + expect(marker.position, equals(const LatLng(0.0, 0.0))); + expect(marker.rotation, equals(0.0)); + expect(marker.visible, equals(true)); + expect(marker.zIndex, equals(0.0)); + expect(marker.onTap, equals(null)); + expect(marker.onDrag, equals(null)); + expect(marker.onDragStart, equals(null)); + expect(marker.onDragEnd, equals(null)); + }); + test('constructor alpha is >= 0.0 and <= 1.0', () { + final ValueSetter initWithAlpha = (double alpha) { + Marker(markerId: MarkerId("ABC123"), alpha: alpha); + }; + expect(() => initWithAlpha(-0.5), throwsAssertionError); + expect(() => initWithAlpha(0.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(0.5), isNot(throwsAssertionError)); + expect(() => initWithAlpha(1.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(100), throwsAssertionError); + }); + + test('toJson', () { + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final Marker marker = Marker( + markerId: MarkerId("ABC123"), + alpha: 0.12345, + anchor: Offset(100, 100), + consumeTapEvents: true, + draggable: true, + flat: true, + icon: testDescriptor, + infoWindow: InfoWindow( + title: "Test title", + snippet: "Test snippet", + anchor: Offset(100, 200), + ), + position: LatLng(50, 50), + rotation: 100, + visible: false, + zIndex: 100, + onTap: () {}, + onDragStart: (LatLng latLng) {}, + onDrag: (LatLng latLng) {}, + onDragEnd: (LatLng latLng) {}, + ); + + final Map json = marker.toJson() as Map; + + expect(json, { + 'markerId': "ABC123", + 'alpha': 0.12345, + 'anchor': [100, 100], + 'consumeTapEvents': true, + 'draggable': true, + 'flat': true, + 'icon': testDescriptor.toJson(), + 'infoWindow': { + 'title': "Test title", + 'snippet': "Test snippet", + 'anchor': [100.0, 200.0], + }, + 'position': [50, 50], + 'rotation': 100.0, + 'visible': false, + 'zIndex': 100.0, + }); + }); + test('clone', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + final Marker clone = marker.clone(); + + expect(identical(clone, marker), isFalse); + expect(clone, equals(marker)); + }); + test('copyWith', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final double testAlphaParam = 0.12345; + final Offset testAnchorParam = Offset(100, 100); + final bool testConsumeTapEventsParam = !marker.consumeTapEvents; + final bool testDraggableParam = !marker.draggable; + final bool testFlatParam = !marker.flat; + final BitmapDescriptor testIconParam = testDescriptor; + final InfoWindow testInfoWindowParam = InfoWindow(title: "Test"); + final LatLng testPositionParam = LatLng(100, 100); + final double testRotationParam = 100; + final bool testVisibleParam = !marker.visible; + final double testZIndexParam = 100; + final List log = []; + + final copy = marker.copyWith( + alphaParam: testAlphaParam, + anchorParam: testAnchorParam, + consumeTapEventsParam: testConsumeTapEventsParam, + draggableParam: testDraggableParam, + flatParam: testFlatParam, + iconParam: testIconParam, + infoWindowParam: testInfoWindowParam, + positionParam: testPositionParam, + rotationParam: testRotationParam, + visibleParam: testVisibleParam, + zIndexParam: testZIndexParam, + onTapParam: () { + log.add("onTapParam"); + }, + onDragStartParam: (LatLng latLng) { + log.add("onDragStartParam"); + }, + onDragParam: (LatLng latLng) { + log.add("onDragParam"); + }, + onDragEndParam: (LatLng latLng) { + log.add("onDragEndParam"); + }, + ); + + expect(copy.alpha, equals(testAlphaParam)); + expect(copy.anchor, equals(testAnchorParam)); + expect(copy.consumeTapEvents, equals(testConsumeTapEventsParam)); + expect(copy.draggable, equals(testDraggableParam)); + expect(copy.flat, equals(testFlatParam)); + expect(copy.icon, equals(testIconParam)); + expect(copy.infoWindow, equals(testInfoWindowParam)); + expect(copy.position, equals(testPositionParam)); + expect(copy.rotation, equals(testRotationParam)); + expect(copy.visible, equals(testVisibleParam)); + expect(copy.zIndex, equals(testZIndexParam)); + + copy.onTap!(); + expect(log, contains("onTapParam")); + + copy.onDragStart!(LatLng(0, 1)); + expect(log, contains("onDragStartParam")); + + copy.onDrag!(LatLng(0, 1)); + expect(log, contains("onDragParam")); + + copy.onDragEnd!(LatLng(0, 1)); + expect(log, contains("onDragEndParam")); + }); + }); +} From d3c892f8261130e0927079076b43008a2ab44a5c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Sep 2021 22:38:20 -0400 Subject: [PATCH 071/259] Remove some trivial custom analysis options files (#4379) Remove a few of the trivial-to-fix legacy analysis option files that were bulk added in the transition to matching flutter/flutter's analysis options. Part of flutter/flutter#76229 --- packages/cross_file/analysis_options.yaml | 1 - .../flutter_plugin_android_lifecycle/analysis_options.yaml | 1 - .../flutter_plugin_android_lifecycle_test.dart | 2 +- .../flutter_plugin_android_lifecycle/example/lib/main.dart | 2 +- .../flutter_plugin_android_lifecycle/example/pubspec.yaml | 4 ++-- packages/plugin_platform_interface/analysis_options.yaml | 1 - packages/plugin_platform_interface/pubspec.yaml | 2 +- .../test/plugin_platform_interface_test.dart | 3 +-- script/configs/custom_analysis.yaml | 3 --- 9 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 packages/cross_file/analysis_options.yaml delete mode 100644 packages/flutter_plugin_android_lifecycle/analysis_options.yaml delete mode 100644 packages/plugin_platform_interface/analysis_options.yaml diff --git a/packages/cross_file/analysis_options.yaml b/packages/cross_file/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/cross_file/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/flutter_plugin_android_lifecycle/analysis_options.yaml b/packages/flutter_plugin_android_lifecycle/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart index 51f9a2537ffc..1d329a02f93b 100644 --- a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart +++ b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter_plugin_android_lifecycle_example/main.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:flutter_plugin_android_lifecycle_example/main.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart index c019590b2a7c..3ef6794dfad2 100644 --- a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart +++ b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart @@ -16,7 +16,7 @@ class MyApp extends StatelessWidget { appBar: AppBar( title: const Text('Sample flutter_plugin_android_lifecycle usage'), ), - body: Center( + body: const Center( child: Text( 'This plugin only provides Android Lifecycle API\n for other Android plugins.')), ), diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index 6dc8d366e82b..0c88cd2c5531 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -17,10 +17,10 @@ dependencies: path: ../ dev_dependencies: - integration_test: - sdk: flutter flutter_test: sdk: flutter + integration_test: + sdk: flutter pedantic: ^1.8.0 flutter: diff --git a/packages/plugin_platform_interface/analysis_options.yaml b/packages/plugin_platform_interface/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/plugin_platform_interface/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 2980a62ee998..66527bc58a61 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -24,5 +24,5 @@ dependencies: dev_dependencies: mockito: ^5.0.0 - test: ^1.16.0 pedantic: ^1.10.0 + test: ^1.16.0 diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart index 0079765f0b09..967fa79d6dc3 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -3,9 +3,8 @@ // found in the LICENSE file. import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/test.dart'; class SamplePluginPlatform extends PlatformInterface { SamplePluginPlatform() : super(token: _token); diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 2b0f844de7e0..2e214757d90d 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -17,15 +17,12 @@ # https://github.com/flutter/flutter/issues/76229 - camera - file_selector -- flutter_plugin_android_lifecycle - google_maps_flutter - google_sign_in - image_picker - in_app_purchase -- integration_test - ios_platform_images - local_auth -- plugin_platform_interface - quick_actions - shared_preferences - url_launcher From 0c282812b194ebf0f8ead790245dc4ed64026b39 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 24 Sep 2021 13:55:44 -0400 Subject: [PATCH 072/259] [webview_flutter] Adjust integration test domains (#4383) Tests that load https://www.google.com and then assert that that's the current URL are failing, because Google is redirecting our tests to a "suspicious activity" page. Switch to example.com, as a domain that is less likely to do traffic-based redirection. --- .../integration_test/webview_flutter_test.dart | 16 ++++++++-------- .../integration_test/webview_flutter_test.dart | 16 ++++++++-------- .../integration_test/webview_flutter_test.dart | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) 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 a6211b2dae75..896366798b18 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 @@ -58,9 +58,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl('https://www.example.com/'); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -1100,11 +1100,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + .evaluateJavascript('location.href = "https://www.example.com/"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1272,11 +1272,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + .evaluateJavascript('location.href = "https://www.example.com"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); }); @@ -1365,10 +1365,10 @@ void main() { pageLoaded = Completer(); await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + .evaluateJavascript('window.open("https://www.example.com/")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion('https://www.example.com/')); expect(controller.canGoBack(), completion(true)); await controller.goBack(); 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 e218d908729c..3dab048c8ff2 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 @@ -64,9 +64,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl('https://www.example.com/'); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -1054,11 +1054,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + .evaluateJavascript('location.href = "https://www.example.com/"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1221,11 +1221,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + .evaluateJavascript('location.href = "https://www.example.com"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); }); @@ -1314,10 +1314,10 @@ void main() { pageLoaded = Completer(); await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + .evaluateJavascript('window.open("https://www.example.com/")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion('https://www.example.com/')); expect(controller.canGoBack(), completion(true)); await controller.goBack(); 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 33d5b340fa5a..c080c46120ec 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 @@ -61,9 +61,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl('https://www.example.com/'); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }, skip: _skipDueToIssue86757); testWidgets('loadUrl with headers', (WidgetTester tester) async { @@ -913,11 +913,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + .evaluateJavascript('location.href = "https://www.example.com/"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1085,11 +1085,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + .evaluateJavascript('location.href = "https://www.example.com"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, 'https://www.example.com/'); }); }); @@ -1176,10 +1176,10 @@ void main() { pageLoaded = Completer(); await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + .evaluateJavascript('window.open("https://www.example.com/")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion('https://www.example.com/')); expect(controller.canGoBack(), completion(true)); await controller.goBack(); From cc3c53cde5abe3f9c122984a14d10a5ff13b5ccc Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 24 Sep 2021 19:58:06 +0200 Subject: [PATCH 073/259] [webview_flutter] Update version number app_facing package (#4375) --- packages/webview_flutter/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/webview_flutter/pubspec.yaml | 2 +- script/configs/exclude_all_plugins_app.yaml | 6 ------ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 99b8a5c419ca..92ac557d01af 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Migrated to fully federated architecture. + ## 2.0.14 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 985f3178916f..ab870639d615 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.14 +version: 2.1.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml index f6246aae2c86..8dd0fde5ef5f 100644 --- a/script/configs/exclude_all_plugins_app.yaml +++ b/script/configs/exclude_all_plugins_app.yaml @@ -8,9 +8,3 @@ # This is a permament entry, as it should never be a direct app dependency. - plugin_platform_interface -# TODO(mvanbeusekom): Remove the exclusion of the webview_flutter_android and -# webview_flutter_wkwebview packages once the native -# implementation is removed from the webview_flutter -# package (see https://github.com/flutter/flutter/issues/86286). -- webview_flutter_android -- webview_flutter_wkwebview From 5ec61962da5e6a9e0f899d19fa66c1fa9233691a Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 27 Sep 2021 11:50:49 -0700 Subject: [PATCH 074/259] [in_app_purchase] Bump dependencies on json_serializable, build_runner (#4386) Regenerate output Also put in a pointer to the json_serializable issue related to creating stand-alone enum converters Co-authored-by: Chris Yang --- .../in_app_purchase_android/CHANGELOG.md | 4 + .../enum_converters.dart | 1 + .../enum_converters.g.dart | 21 ++- .../purchase_wrapper.g.dart | 109 +++++++-------- .../sku_details_wrapper.g.dart | 78 +++++------ .../in_app_purchase_android/pubspec.yaml | 4 +- .../in_app_purchase_ios/CHANGELOG.md | 4 + .../store_kit_wrappers/enum_converters.dart | 1 + .../store_kit_wrappers/enum_converters.g.dart | 16 +-- .../sk_payment_queue_wrapper.g.dart | 36 +++-- .../sk_payment_transaction_wrappers.g.dart | 33 +++-- .../sk_product_wrapper.g.dart | 129 +++++++++--------- .../sk_storefront_wrapper.g.dart | 11 +- .../in_app_purchase_ios/pubspec.yaml | 4 +- 14 files changed, 218 insertions(+), 233 deletions(-) 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 7a998d0547de..10c77cb6b4d3 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,7 @@ +## NEXT + +* Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. + ## 0.1.4+7 * Ensure that the `SkuDetailsWrapper.introductoryPriceMicros` is populated correctly. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart index 7ff333098fcc..931d92f7b1e7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart @@ -132,6 +132,7 @@ class BillingClientFeatureConverter } // Define a class so we generate serializer helper methods for the enums +// See https://github.com/google/json_serializable.dart/issues/778 @JsonSerializable() class _SerializedEnums { late BillingResponse response; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart index 8d667d035196..fe92f56653e4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -6,19 +6,16 @@ part of 'enum_converters.dart'; // JsonSerializableGenerator // ************************************************************************** -_SerializedEnums _$_SerializedEnumsFromJson(Map json) { - return _SerializedEnums() - ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) - ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) - ..purchaseState = - _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) - ..prorationMode = - _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']) - ..billingClientFeature = _$enumDecode( - _$BillingClientFeatureEnumMap, json['billingClientFeature']); -} +_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() + ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) + ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) + ..purchaseState = + _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) + ..prorationMode = _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']) + ..billingClientFeature = + _$enumDecode(_$BillingClientFeatureEnumMap, json['billingClientFeature']); -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => +Map _$SerializedEnumsToJson(_SerializedEnums instance) => { 'response': _$BillingResponseEnumMap[instance.response], 'type': _$SkuTypeEnumMap[instance.type], diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index 5607dbdd8cb2..b5d9fe8cd3af 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -6,24 +6,22 @@ part of 'purchase_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { - return PurchaseWrapper( - orderId: json['orderId'] as String? ?? '', - packageName: json['packageName'] as String? ?? '', - purchaseTime: json['purchaseTime'] as int? ?? 0, - purchaseToken: json['purchaseToken'] as String? ?? '', - signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', - isAutoRenewing: json['isAutoRenewing'] as bool, - originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String?, - isAcknowledged: json['isAcknowledged'] as bool? ?? false, - purchaseState: - const PurchaseStateConverter().fromJson(json['purchaseState'] as int?), - obfuscatedAccountId: json['obfuscatedAccountId'] as String?, - obfuscatedProfileId: json['obfuscatedProfileId'] as String?, - ); -} +PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( + orderId: json['orderId'] as String? ?? '', + packageName: json['packageName'] as String? ?? '', + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', + isAutoRenewing: json['isAutoRenewing'] as bool, + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, + isAcknowledged: json['isAcknowledged'] as bool? ?? false, + purchaseState: const PurchaseStateConverter() + .fromJson(json['purchaseState'] as int?), + obfuscatedAccountId: json['obfuscatedAccountId'] as String?, + obfuscatedProfileId: json['obfuscatedProfileId'] as String?, + ); Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => { @@ -43,16 +41,15 @@ Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => 'obfuscatedProfileId': instance.obfuscatedProfileId, }; -PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { - return PurchaseHistoryRecordWrapper( - purchaseTime: json['purchaseTime'] as int? ?? 0, - purchaseToken: json['purchaseToken'] as String? ?? '', - signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', - originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String?, - ); -} +PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => + PurchaseHistoryRecordWrapper( + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, + ); Map _$PurchaseHistoryRecordWrapperToJson( PurchaseHistoryRecordWrapper instance) => @@ -65,21 +62,20 @@ Map _$PurchaseHistoryRecordWrapperToJson( 'developerPayload': instance.developerPayload, }; -PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { - return PurchasesResultWrapper( - responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int?), - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - purchasesList: (json['purchasesList'] as List?) - ?.map((e) => - PurchaseWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - ); -} +PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) => + PurchasesResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int?), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchasesList: (json['purchasesList'] as List?) + ?.map((e) => + PurchaseWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], + ); Map _$PurchasesResultWrapperToJson( PurchasesResultWrapper instance) => @@ -90,20 +86,19 @@ Map _$PurchasesResultWrapperToJson( 'purchasesList': instance.purchasesList, }; -PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { - return PurchasesHistoryResult( - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - purchaseHistoryRecordList: - (json['purchaseHistoryRecordList'] as List?) - ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - ); -} +PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) => + PurchasesHistoryResult( + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchaseHistoryRecordList: + (json['purchaseHistoryRecordList'] as List?) + ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); Map _$PurchasesHistoryResultToJson( PurchasesHistoryResult instance) => diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index d21f832a2de6..1fc450ed6933 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -6,27 +6,25 @@ part of 'sku_details_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { - return SkuDetailsWrapper( - description: json['description'] as String? ?? '', - freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', - introductoryPrice: json['introductoryPrice'] as String? ?? '', - introductoryPriceMicros: - json['introductoryPriceAmountMicros'] as String? ?? '', - introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, - introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', - price: json['price'] as String? ?? '', - priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, - priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', - priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', - sku: json['sku'] as String? ?? '', - subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', - title: json['title'] as String? ?? '', - type: const SkuTypeConverter().fromJson(json['type'] as String?), - originalPrice: json['originalPrice'] as String? ?? '', - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, - ); -} +SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( + description: json['description'] as String? ?? '', + freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', + introductoryPrice: json['introductoryPrice'] as String? ?? '', + introductoryPriceMicros: + json['introductoryPriceAmountMicros'] as String? ?? '', + introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, + introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', + price: json['price'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', + sku: json['sku'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', + title: json['title'] as String? ?? '', + type: const SkuTypeConverter().fromJson(json['type'] as String?), + originalPrice: json['originalPrice'] as String? ?? '', + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, + ); Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => { @@ -48,19 +46,18 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => 'originalPriceAmountMicros': instance.originalPriceAmountMicros, }; -SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { - return SkuDetailsResponseWrapper( - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - skuDetailsList: (json['skuDetailsList'] as List?) - ?.map((e) => - SkuDetailsWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - ); -} +SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => + SkuDetailsResponseWrapper( + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + skuDetailsList: (json['skuDetailsList'] as List?) + ?.map((e) => SkuDetailsWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); Map _$SkuDetailsResponseWrapperToJson( SkuDetailsResponseWrapper instance) => @@ -69,13 +66,12 @@ Map _$SkuDetailsResponseWrapperToJson( 'skuDetailsList': instance.skuDetailsList, }; -BillingResultWrapper _$BillingResultWrapperFromJson(Map json) { - return BillingResultWrapper( - responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int?), - debugMessage: json['debugMessage'] as String?, - ); -} +BillingResultWrapper _$BillingResultWrapperFromJson(Map json) => + BillingResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int?), + debugMessage: json['debugMessage'] as String?, + ); Map _$BillingResultWrapperToJson( BillingResultWrapper instance) => 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 64a40889f375..3a60abbae19d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -25,8 +25,8 @@ dependencies: meta: ^1.3.0 dev_dependencies: - build_runner: ^1.11.1 + build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^4.1.1 + json_serializable: ^5.0.2 test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 04509e56ecde..7e3902b206ab 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. + ## 0.1.3+4 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index 08af2c6058c4..70178260febf 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -101,6 +101,7 @@ class SKProductDiscountPaymentModeConverter } // Define a class so we generate serializer helper methods for the enums +// See https://github.com/google/json_serializable.dart/issues/778 @JsonSerializable() class _SerializedEnums { late SKPaymentTransactionStateWrapper response; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart index b003f435a800..ce0f56ba4d34 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -6,16 +6,14 @@ part of 'enum_converters.dart'; // JsonSerializableGenerator // ************************************************************************** -_SerializedEnums _$_SerializedEnumsFromJson(Map json) { - return _SerializedEnums() - ..response = _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap, json['response']) - ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) - ..discountPaymentMode = _$enumDecode( - _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); -} +_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() + ..response = + _$enumDecode(_$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => +Map _$SerializedEnumsToJson(_SerializedEnums instance) => { 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 2b886597adc5..4d2b5e4b3d4b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -6,16 +6,14 @@ part of 'sk_payment_queue_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -SKError _$SKErrorFromJson(Map json) { - return SKError( - code: json['code'] as int? ?? 0, - domain: json['domain'] as String? ?? '', - userInfo: (json['userInfo'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - ) ?? - {}, - ); -} +SKError _$SKErrorFromJson(Map json) => SKError( + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, + ); Map _$SKErrorToJson(SKError instance) => { 'code': instance.code, @@ -23,16 +21,14 @@ Map _$SKErrorToJson(SKError instance) => { 'userInfo': instance.userInfo, }; -SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { - return SKPaymentWrapper( - productIdentifier: json['productIdentifier'] as String? ?? '', - applicationUsername: json['applicationUsername'] as String?, - requestData: json['requestData'] as String?, - quantity: json['quantity'] as int? ?? 0, - simulatesAskToBuyInSandbox: - json['simulatesAskToBuyInSandbox'] as bool? ?? false, - ); -} +SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) => SKPaymentWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, + ); Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => { diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart index 4c7af21bc151..fd10d9ad977b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -6,23 +6,22 @@ part of 'sk_payment_transaction_wrappers.dart'; // JsonSerializableGenerator // ************************************************************************** -SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { - return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper.fromJson( - Map.from(json['payment'] as Map)), - transactionState: const SKTransactionStatusConverter() - .fromJson(json['transactionState'] as int?), - originalTransaction: json['originalTransaction'] == null - ? null - : SKPaymentTransactionWrapper.fromJson( - Map.from(json['originalTransaction'] as Map)), - transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), - transactionIdentifier: json['transactionIdentifier'] as String?, - error: json['error'] == null - ? null - : SKError.fromJson(Map.from(json['error'] as Map)), - ); -} +SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) => + SKPaymentTransactionWrapper( + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), + transactionState: const SKTransactionStatusConverter() + .fromJson(json['transactionState'] as int?), + originalTransaction: json['originalTransaction'] == null + ? null + : SKPaymentTransactionWrapper.fromJson( + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), + ); Map _$SKPaymentTransactionWrapperToJson( SKPaymentTransactionWrapper instance) => diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index 66f4b7827c38..485bf1932efa 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -6,20 +6,19 @@ part of 'sk_product_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) { - return SkProductResponseWrapper( - products: (json['products'] as List?) - ?.map((e) => - SKProductWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - invalidProductIdentifiers: - (json['invalidProductIdentifiers'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - ); -} +SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) => + SkProductResponseWrapper( + products: (json['products'] as List?) + ?.map((e) => SKProductWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); Map _$SkProductResponseWrapperToJson( SkProductResponseWrapper instance) => @@ -29,13 +28,12 @@ Map _$SkProductResponseWrapperToJson( }; SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( - Map json) { - return SKProductSubscriptionPeriodWrapper( - numberOfUnits: json['numberOfUnits'] as int? ?? 0, - unit: const SKSubscriptionPeriodUnitConverter() - .fromJson(json['unit'] as int?), - ); -} + Map json) => + SKProductSubscriptionPeriodWrapper( + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), + ); Map _$SKProductSubscriptionPeriodWrapperToJson( SKProductSubscriptionPeriodWrapper instance) => @@ -44,22 +42,21 @@ Map _$SKProductSubscriptionPeriodWrapperToJson( 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), }; -SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { - return SKProductDiscountWrapper( - price: json['price'] as String? ?? '', - priceLocale: - SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, - paymentMode: const SKProductDiscountPaymentModeConverter() - .fromJson(json['paymentMode'] as int?), - subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( - (json['subscriptionPeriod'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - ); -} +SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => + SKProductDiscountWrapper( + price: json['price'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, + paymentMode: const SKProductDiscountPaymentModeConverter() + .fromJson(json['paymentMode'] as int?), + subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + ); Map _$SKProductDiscountWrapperToJson( SKProductDiscountWrapper instance) => @@ -72,29 +69,28 @@ Map _$SKProductDiscountWrapperToJson( 'subscriptionPeriod': instance.subscriptionPeriod, }; -SKProductWrapper _$SKProductWrapperFromJson(Map json) { - return SKProductWrapper( - productIdentifier: json['productIdentifier'] as String? ?? '', - localizedTitle: json['localizedTitle'] as String? ?? '', - localizedDescription: json['localizedDescription'] as String? ?? '', - priceLocale: - SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String?, - price: json['price'] as String? ?? '', - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - (json['subscriptionPeriod'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - introductoryPrice: json['introductoryPrice'] == null - ? null - : SKProductDiscountWrapper.fromJson( - Map.from(json['introductoryPrice'] as Map)), - ); -} +SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + localizedTitle: json['localizedTitle'] as String? ?? '', + localizedDescription: json['localizedDescription'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + subscriptionGroupIdentifier: + json['subscriptionGroupIdentifier'] as String?, + price: json['price'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] == null + ? null + : SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + introductoryPrice: json['introductoryPrice'] == null + ? null + : SKProductDiscountWrapper.fromJson( + Map.from(json['introductoryPrice'] as Map)), + ); Map _$SKProductWrapperToJson(SKProductWrapper instance) => { @@ -108,13 +104,12 @@ Map _$SKProductWrapperToJson(SKProductWrapper instance) => 'introductoryPrice': instance.introductoryPrice, }; -SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) { - return SKPriceLocaleWrapper( - currencySymbol: json['currencySymbol'] as String? ?? '', - currencyCode: json['currencyCode'] as String? ?? '', - countryCode: json['countryCode'] as String? ?? '', - ); -} +SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) => + SKPriceLocaleWrapper( + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', + countryCode: json['countryCode'] as String? ?? '', + ); Map _$SKPriceLocaleWrapperToJson( SKPriceLocaleWrapper instance) => diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart index f75cfc5711e8..b2d5d3a06d1d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart @@ -6,12 +6,11 @@ part of 'sk_storefront_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) { - return SKStorefrontWrapper( - countryCode: json['countryCode'] as String, - identifier: json['identifier'] as String, - ); -} +SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) => + SKStorefrontWrapper( + countryCode: json['countryCode'] as String, + identifier: json['identifier'] as String, + ); Map _$SKStorefrontWrapperToJson( SKStorefrontWrapper instance) => diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 9ba642e2e590..30a57bb56c94 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -24,8 +24,8 @@ dependencies: meta: ^1.3.0 dev_dependencies: - build_runner: ^1.11.1 + build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^4.1.1 + json_serializable: ^5.0.2 test: ^1.16.0 From 80b5d2ed3f3107180e4e4032a7633a3cf3fcf05f Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 27 Sep 2021 23:30:09 +0200 Subject: [PATCH 075/259] [ci] Temporary run publish task on Flutter stable channel. (#4388) --- .cirrus.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 67343ee15f88..ef6b9c1b6d44 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -69,6 +69,9 @@ task: - dart pub run test - name: publishable env: + # TODO (mvanbeusekom): Temporary override to "stable" because of failure on "master". + # Remove override once https://github.com/dart-lang/pub/issues/3152 is resolved. + CHANNEL: stable CHANGE_DESC: "$TMPDIR/change-description.txt" version_check_script: # For pre-submit, pass the PR description to the script to allow for From f58ab59880d76f625df07a2cd48f176729aec834 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 28 Sep 2021 00:38:04 +0200 Subject: [PATCH 076/259] Load navigation controls immediately. (#4378) --- .../webview_flutter_wkwebview/CHANGELOG.md | 4 ++++ .../webview_flutter_wkwebview/example/lib/main.dart | 8 ++++---- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 1a85bc8a53e5..242d79b4bd82 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + ## 2.0.13 * Extract WKWebView implementation from `webview_flutter`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index a953e062ded5..15b4cfc7c549 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -289,7 +289,7 @@ class _NavigationControls extends StatelessWidget { final bool webViewReady = snapshot.connectionState == ConnectionState.done; final WebViewController? controller = snapshot.data; - if (controller == null) return Container(); + return Row( children: [ IconButton( @@ -297,7 +297,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { // ignore: deprecated_member_use @@ -313,7 +313,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + if (await controller!.canGoForward()) { await controller.goForward(); } else { // ignore: deprecated_member_use @@ -330,7 +330,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index c6f6d6f94f07..a7305cea7a94 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.13 +version: 2.0.14 environment: sdk: ">=2.14.0 <3.0.0" From 8a71e0ee47c770f29977f6c3810867cb4d1e4376 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 28 Sep 2021 09:08:06 +0200 Subject: [PATCH 077/259] [camera] Fix IllegalStateException being thrown in Android implementation when switching activities. (#4319) --- packages/camera/camera/CHANGELOG.md | 4 ++ packages/camera/camera/android/build.gradle | 2 +- .../io/flutter/plugins/camera/Camera.java | 51 +++++++++++++++---- .../flutter/plugins/camera/CameraPlugin.java | 18 ++----- .../plugins/camera/MethodCallHandlerImpl.java | 17 +------ .../io/flutter/plugins/camera/CameraTest.java | 43 ++++++++++++++++ .../camera/MethodCallHandlerImplTest.java | 12 ++++- packages/camera/camera/pubspec.yaml | 2 +- 8 files changed, 107 insertions(+), 42 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 62b5f1f9bd4c..b2dda9a52436 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+1 + +* Fixed Android implementation throwing IllegalStateException when switching to a different activity. + ## 0.9.4 * Add web support by endorsing `package:camera_web`. diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 61d13e5579cc..633efd0b284a 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -60,7 +60,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'org.mockito:mockito-inline:3.12.4' testImplementation 'androidx.test:core:1.3.0' testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4601e7d34d69..75ced531b08a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -35,9 +35,7 @@ import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; +import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel; @@ -82,8 +80,7 @@ interface ErrorCallback { class Camera implements CameraCaptureCallback.CameraCaptureStateListener, - ImageReader.OnImageAvailableListener, - LifecycleObserver { + ImageReader.OnImageAvailableListener { private static final String TAG = "Camera"; private static final HashMap supportedImageFormats; @@ -576,19 +573,21 @@ private Display getDefaultDisplay() { } /** Starts a background thread and its {@link Handler}. */ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void startBackgroundThread() { - backgroundHandlerThread = new HandlerThread("CameraBackground"); + if (backgroundHandlerThread != null) { + return; + } + + backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground"); try { backgroundHandlerThread.start(); } catch (IllegalThreadStateException e) { // Ignore exception in case the thread has already started. } - backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); + backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper()); } /** Stops the background thread and its {@link Handler}. */ - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void stopBackgroundThread() { if (backgroundHandlerThread != null) { backgroundHandlerThread.quitSafely(); @@ -1120,4 +1119,38 @@ public void dispose() { flutterTexture.release(); getDeviceOrientationManager().stop(); } + + /** Factory class that assists in creating a {@link HandlerThread} instance. */ + static class HandlerThreadFactory { + /** + * Creates a new instance of the {@link HandlerThread} class. + * + *

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

This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param looper to give to the Handler. + * @return new instance of the {@link Handler} class. + */ + @VisibleForTesting + public static Handler create(Looper looper) { + return new Handler(looper); + } + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index ef3a2b9b5d83..067ed0295e2e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -8,11 +8,9 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; @@ -53,8 +51,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.activity(), registrar.messenger(), registrar::addRequestPermissionsResultListener, - registrar.view(), - null); + registrar.view()); } @Override @@ -73,8 +70,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getTextureRegistry(), - FlutterLifecycleAdapter.getActivityLifecycle(binding)); + flutterPluginBinding.getTextureRegistry()); } @Override @@ -100,8 +96,7 @@ private void maybeStartListening( Activity activity, BinaryMessenger messenger, PermissionsRegistry permissionsRegistry, - TextureRegistry textureRegistry, - @Nullable Lifecycle lifecycle) { + TextureRegistry textureRegistry) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin. return; @@ -109,11 +104,6 @@ private void maybeStartListening( methodCallHandler = new MethodCallHandlerImpl( - activity, - messenger, - new CameraPermissions(), - permissionsRegistry, - textureRegistry, - lifecycle); + activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 5e25353cbca9..35cc2b081bae 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -10,8 +10,6 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -29,7 +27,7 @@ import java.util.HashMap; import java.util.Map; -final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver { +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; private final BinaryMessenger messenger; private final CameraPermissions cameraPermissions; @@ -37,7 +35,6 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Li private final TextureRegistry textureRegistry; private final MethodChannel methodChannel; private final EventChannel imageStreamChannel; - private final Lifecycle lifecycle; private @Nullable Camera camera; MethodCallHandlerImpl( @@ -45,14 +42,12 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Li BinaryMessenger messenger, CameraPermissions cameraPermissions, PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry, - @Nullable Lifecycle lifecycle) { + TextureRegistry textureRegistry) { this.activity = activity; this.messenger = messenger; this.cameraPermissions = cameraPermissions; this.permissionsRegistry = permissionsAdder; this.textureRegistry = textureRegistry; - this.lifecycle = lifecycle; methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); @@ -387,10 +382,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); - if (camera != null && lifecycle != null) { - lifecycle.removeObserver(camera); - } - camera = new Camera( activity, @@ -401,10 +392,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce resolutionPreset, enableAudio); - if (lifecycle != null) { - lifecycle.addObserver(camera); - } - Map reply = new HashMap<>(); reply.put("cameraId", flutterSurfaceTexture.id()); result.success(reply); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index fbed28bc11fc..9d973195435e 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -5,11 +5,13 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -23,7 +25,10 @@ import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleObserver; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.CameraFeatureFactory; @@ -49,6 +54,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; public class CameraTest { private CameraProperties mockCameraProperties; @@ -57,6 +63,10 @@ public class CameraTest { private Camera camera; private CameraCaptureSession mockCaptureSession; private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; @Before public void before() { @@ -65,6 +75,10 @@ public void before() { mockDartMessenger = mock(DartMessenger.class); mockCaptureSession = mock(CameraCaptureSession.class); mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class); + mockHandlerThreadFactory = mockStatic(Camera.HandlerThreadFactory.class); + mockHandlerThread = mock(HandlerThread.class); + mockHandlerFactory = mockStatic(Camera.HandlerFactory.class); + mockHandler = mock(Handler.class); final Activity mockActivity = mock(Activity.class); final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = @@ -74,6 +88,10 @@ public void before() { final boolean enableAudio = false; when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + mockHandlerFactory.when(() -> Camera.HandlerFactory.create(any())).thenReturn(mockHandler); + mockHandlerThreadFactory + .when(() -> Camera.HandlerThreadFactory.create(any())) + .thenReturn(mockHandlerThread); camera = new Camera( @@ -92,6 +110,15 @@ public void before() { @After public void after() { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + mockHandlerThreadFactory.close(); + mockHandlerFactory.close(); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class cameraClass = Camera.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass)); } @Test @@ -773,6 +800,22 @@ public void resumePreview_shouldSendErrorEventOnCameraAccessException() verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); } + @Test + public void startBackgroundThread_shouldStartNewThread() { + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + assertEquals(mockHandler, TestUtils.getPrivateField(camera, "backgroundHandler")); + } + + @Test + public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() { + camera.startBackgroundThread(); + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + } + private static class TestCameraFeatureFactory implements CameraFeatureFactory { private final AutoFocusFeature mockAutoFocusFeature; private final ExposureLockFeature mockExposureLockFeature; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java index 35eed7a66a1a..868e2e9e6d57 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java @@ -4,6 +4,7 @@ package io.flutter.plugins.camera; +import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -11,6 +12,7 @@ import android.app.Activity; import android.hardware.camera2.CameraAccessException; +import androidx.lifecycle.LifecycleObserver; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -33,13 +35,19 @@ public void setUp() { mock(BinaryMessenger.class), mock(CameraPermissions.class), mock(CameraPermissions.PermissionsRegistry.class), - mock(TextureRegistry.class), - null); + mock(TextureRegistry.class)); mockResult = mock(MethodChannel.Result.class); mockCamera = mock(Camera.class); TestUtils.setPrivateField(handler, "camera", mockCamera); } + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class methodCallHandlerClass = MethodCallHandlerImpl.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass)); + } + @Test public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() throws CameraAccessException { diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index b8894d58ac3a..5c225eaee48f 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4 +version: 0.9.4+1 environment: sdk: ">=2.14.0 <3.0.0" From 326e3c4f2765184978038eb55e5cf2785c9d690d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 28 Sep 2021 09:13:04 +0200 Subject: [PATCH 078/259] Load navigation controls immediately. (#4377) --- .../webview_flutter/webview_flutter_android/CHANGELOG.md | 4 ++++ .../webview_flutter_android/example/lib/main.dart | 8 ++++---- .../webview_flutter/webview_flutter_android/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index d6a10e9b918a..917a3c73acb1 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + ## 2.0.13 * Extract Android implementation from `webview_flutter`. 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 6176ce255eb9..65f49716aaac 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -289,7 +289,7 @@ class _NavigationControls extends StatelessWidget { final bool webViewReady = snapshot.connectionState == ConnectionState.done; final WebViewController? controller = snapshot.data; - if (controller == null) return Container(); + return Row( children: [ IconButton( @@ -297,7 +297,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { // ignore: deprecated_member_use @@ -313,7 +313,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + if (await controller!.canGoForward()) { await controller.goForward(); } else { // ignore: deprecated_member_use @@ -330,7 +330,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index f7db4c6fb63a..4a3d4cb1d942 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.13 +version: 2.0.14 environment: sdk: ">=2.14.0 <3.0.0" From f48f8dba59fb624154ca19834f31dbbc3ac0769a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 28 Sep 2021 09:23:05 +0200 Subject: [PATCH 079/259] [in_app_purchase] Ensure the introductoryPriceMicros field is transported as a String. (#4370) --- .../in_app_purchase_android/CHANGELOG.md | 3 +- .../sku_details_wrapper.dart | 28 ++++++-- .../sku_details_wrapper.g.dart | 6 +- .../in_app_purchase_android/pubspec.yaml | 2 +- .../sku_details_wrapper_deprecated_test.dart | 69 +++++++++++++++++++ .../sku_details_wrapper_test.dart | 4 +- 6 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.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 10c77cb6b4d3..0c75ae3cf819 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.5 +* Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field. * Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. ## 0.1.4+7 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 9c349badbb04..754f7a352f1c 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 @@ -32,7 +32,9 @@ class SkuDetailsWrapper { required this.description, required this.freeTrialPeriod, required this.introductoryPrice, - required this.introductoryPriceMicros, + @Deprecated('Use `introductoryPriceAmountMicros` parameter instead') + String introductoryPriceMicros = '', + this.introductoryPriceAmountMicros = 0, required this.introductoryPriceCycles, required this.introductoryPricePeriod, required this.price, @@ -45,7 +47,9 @@ class SkuDetailsWrapper { required this.type, required this.originalPrice, required this.originalPriceAmountMicros, - }); + }) : _introductoryPriceMicros = introductoryPriceMicros; + + final String _introductoryPriceMicros; /// Constructs an instance of this from a key value map of data. /// @@ -67,9 +71,19 @@ class SkuDetailsWrapper { @JsonKey(defaultValue: '') final String introductoryPrice; - /// [introductoryPrice] in micro-units 990000 - @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: '') - final String introductoryPriceMicros; + /// [introductoryPrice] in micro-units 990000. + /// + /// Returns 0 if the SKU is not a subscription or doesn't have an introductory + /// period. + @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: 0) + final int introductoryPriceAmountMicros; + + /// String representation of [introductoryPrice] in micro-units 990000 + @Deprecated('Use `introductoryPriceAmountMicros` instead.') + @JsonKey(ignore: true) + String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty + ? introductoryPriceAmountMicros.toString() + : _introductoryPriceMicros; /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. /// Returns 0 if the SKU is not a subscription or doesn't have an introductory period. @@ -131,7 +145,7 @@ class SkuDetailsWrapper { other.description == description && other.freeTrialPeriod == freeTrialPeriod && other.introductoryPrice == introductoryPrice && - other.introductoryPriceMicros == introductoryPriceMicros && + other.introductoryPriceAmountMicros == introductoryPriceAmountMicros && other.introductoryPriceCycles == introductoryPriceCycles && other.introductoryPricePeriod == introductoryPricePeriod && other.price == price && @@ -150,7 +164,7 @@ class SkuDetailsWrapper { description.hashCode, freeTrialPeriod.hashCode, introductoryPrice.hashCode, - introductoryPriceMicros.hashCode, + introductoryPriceAmountMicros.hashCode, introductoryPriceCycles.hashCode, introductoryPricePeriod.hashCode, price.hashCode, diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 1fc450ed6933..53d5931ecb56 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -10,8 +10,8 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( description: json['description'] as String? ?? '', freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', introductoryPrice: json['introductoryPrice'] as String? ?? '', - introductoryPriceMicros: - json['introductoryPriceAmountMicros'] as String? ?? '', + introductoryPriceAmountMicros: + json['introductoryPriceAmountMicros'] as int? ?? 0, introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', price: json['price'] as String? ?? '', @@ -31,7 +31,7 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => 'description': instance.description, 'freeTrialPeriod': instance.freeTrialPeriod, 'introductoryPrice': instance.introductoryPrice, - 'introductoryPriceAmountMicros': instance.introductoryPriceMicros, + 'introductoryPriceAmountMicros': instance.introductoryPriceAmountMicros, 'introductoryPriceCycles': instance.introductoryPriceCycles, 'introductoryPricePeriod': instance.introductoryPricePeriod, 'price': instance.price, 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 3a60abbae19d..33f51cc70feb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+7 +version: 0.1.5 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart new file mode 100644 index 000000000000..3e29d92724ad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(mvanbeusekom): Remove this file when the deprecated +// `SkuDetailsWrapper.introductoryPriceMicros` field is +// removed. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +void main() { + test( + 'Deprecated `introductoryPriceMicros` field reflects parameter from constructor', + () { + final SkuDetailsWrapper skuDetails = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + // ignore: deprecated_member_use_from_same_package + introductoryPriceMicros: '990000', + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + + expect(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 0); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); + + test( + '`introductoryPriceAmoutMicros` constructor parameter is reflected by deprecated `introductoryPriceMicros` and `introductoryPriceAmountMicros` fields', + () { + final SkuDetailsWrapper skuDetails = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + introductoryPriceAmountMicros: 990000, + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + + expect(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 990000); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart index 62d9104f3738..18804a41940e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -11,7 +11,7 @@ final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( description: 'description', freeTrialPeriod: 'freeTrialPeriod', introductoryPrice: 'introductoryPrice', - introductoryPriceMicros: 'introductoryPriceMicros', + introductoryPriceAmountMicros: 990000, introductoryPriceCycles: 1, introductoryPricePeriod: 'introductoryPricePeriod', price: 'price', @@ -134,7 +134,7 @@ Map buildSkuMap(SkuDetailsWrapper original) { 'description': original.description, 'freeTrialPeriod': original.freeTrialPeriod, 'introductoryPrice': original.introductoryPrice, - 'introductoryPriceAmountMicros': original.introductoryPriceMicros, + 'introductoryPriceAmountMicros': original.introductoryPriceAmountMicros, 'introductoryPriceCycles': original.introductoryPriceCycles, 'introductoryPricePeriod': original.introductoryPricePeriod, 'price': original.price, From 44706929299d759ada6864d2a3ab9fe45398ab08 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 28 Sep 2021 11:23:02 +0200 Subject: [PATCH 080/259] Fixed _CastError when running example App (#4390) --- packages/webview_flutter/webview_flutter/CHANGELOG.md | 4 ++++ .../webview_flutter/webview_flutter/example/lib/main.dart | 8 ++++---- packages/webview_flutter/webview_flutter/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 92ac557d01af..6724b43476ff 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Fixed `_CastError` that was thrown when running the example App. + ## 2.1.0 * Migrated to fully federated architecture. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 88256cc66287..c456a9691455 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -302,7 +302,7 @@ class NavigationControls extends StatelessWidget { (BuildContext context, AsyncSnapshot snapshot) { final bool webViewReady = snapshot.connectionState == ConnectionState.done; - final WebViewController controller = snapshot.data!; + final WebViewController? controller = snapshot.data; return Row( children: [ IconButton( @@ -310,7 +310,7 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { // ignore: deprecated_member_use @@ -326,7 +326,7 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + if (await controller!.canGoForward()) { await controller.goForward(); } else { // ignore: deprecated_member_use @@ -343,7 +343,7 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index ab870639d615..4206789cb1b3 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.14.0 <3.0.0" From f63395d86dbb5acc7488824fc5d81055bdebc041 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 28 Sep 2021 09:20:20 -0400 Subject: [PATCH 081/259] [flutter_plugin_tools] Check licenses in Kotlin (#4373) The license check overlooked Kotlin, since it's not currently widely used in our repositories. Also adds the missing license to one Kotlin file, from an example that was (likely accidentally) re-generated using Kotlin instead of Java. --- .../main/kotlin/io/flutter/plugins/example/MainActivity.kt | 4 ++++ script/tool/CHANGELOG.md | 1 + script/tool/lib/src/license_check_command.dart | 1 + script/tool/test/license_check_command_test.dart | 1 + 4 files changed, 7 insertions(+) diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt index 9059dae9e4c4..50cad6f36e24 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.example import io.flutter.embedding.android.FlutterActivity diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a5263ba03965..6119545260aa 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -9,6 +9,7 @@ - Added flags to `version-check` to allow overriding the platform interface major version change restriction. - Improved error handling and error messages in CHANGELOG version checks. +- `license-check` now validates Kotlin files. ## 0.7.1 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 8cee46b45a4c..7165e985c059 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -16,6 +16,7 @@ const Set _codeFileExtensions = { '.h', '.html', '.java', + '.kt', '.m', '.mm', '.swift', diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 288cf4696a59..5a8a90e9a674 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -66,6 +66,7 @@ void main() { 'html': true, 'java': true, 'json': false, + 'kt': true, 'm': true, 'md': false, 'mm': true, From 1ef44050141b8792050bfbde6cab0916f558f254 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 28 Sep 2021 15:45:08 -0400 Subject: [PATCH 082/259] [file_selector] Remove custom analysis options (#4382) Switches to the new repo-standard analysis options, and fixes all violations. Part of https://github.com/flutter/flutter/issues/76229 --- packages/file_selector/analysis_options.yaml | 1 - .../file_selector/file_selector/CHANGELOG.md | 4 + .../example/lib/get_directory_page.dart | 22 ++-- .../file_selector/example/lib/home_page.dart | 20 +-- .../file_selector/example/lib/main.dart | 18 +-- .../example/lib/open_image_page.dart | 26 ++-- .../lib/open_multiple_images_page.dart | 31 ++--- .../example/lib/open_text_page.dart | 23 ++-- .../example/lib/save_text_page.dart | 16 +-- .../file_selector/example/pubspec.yaml | 5 +- .../test/file_selector_test.dart | 76 ++++++------ .../CHANGELOG.md | 4 + .../method_channel_file_selector.dart | 20 +-- .../src/types/x_type_group/x_type_group.dart | 7 +- .../lib/src/web_helpers/web_helpers.dart | 4 +- ...file_selector_platform_interface_test.dart | 3 +- .../method_channel_file_selector_test.dart | 117 ++++++++++-------- .../test/x_type_group_test.dart | 26 ++-- .../file_selector_web/CHANGELOG.md | 4 + .../integration_test/dom_helper_test.dart | 25 ++-- .../file_selector_web_test.dart | 51 ++++---- .../file_selector_web/example/lib/main.dart | 2 +- .../lib/file_selector_web.dart | 22 ++-- .../file_selector_web/lib/src/dom_helper.dart | 17 +-- .../file_selector_web/lib/src/utils.dart | 8 +- .../file_selector_web/test/utils_test.dart | 45 +++---- script/configs/custom_analysis.yaml | 1 - 27 files changed, 321 insertions(+), 277 deletions(-) delete mode 100644 packages/file_selector/analysis_options.yaml diff --git a/packages/file_selector/analysis_options.yaml b/packages/file_selector/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/file_selector/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 2f8a4d0754f1..225f601c3ee6 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Minor code cleanup for new analysis rules. + ## 0.8.2 * Update platform_plugin_interface version requirement. diff --git a/packages/file_selector/file_selector/example/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart index 6022559ce40e..b3ed9d0eeaca 100644 --- a/packages/file_selector/file_selector/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; /// Screen that shows an example of getDirectoryPath class GetDirectoryPage extends StatelessWidget { - void _getDirectoryPath(BuildContext context) async { - final String confirmButtonText = 'Choose'; + Future _getDirectoryPath(BuildContext context) async { + const String confirmButtonText = 'Choose'; final String? directoryPath = await getDirectoryPath( confirmButtonText: confirmButtonText, ); @@ -16,9 +16,9 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( + await showDialog( context: context, - builder: (context) => TextDisplay(directoryPath), + builder: (BuildContext context) => TextDisplay(directoryPath), ); } @@ -26,7 +26,7 @@ class GetDirectoryPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open a text file"), + title: const Text('Open a text file'), ), body: Center( child: Column( @@ -37,7 +37,7 @@ class GetDirectoryPage extends StatelessWidget { primary: Colors.blue, onPrimary: Colors.white, ), - child: Text('Press to ask user to choose a directory'), + child: const Text('Press to ask user to choose a directory'), onPressed: () => _getDirectoryPath(context), ), ], @@ -49,22 +49,22 @@ class GetDirectoryPage extends StatelessWidget { /// Widget that displays a text file in a dialog class TextDisplay extends StatelessWidget { + /// Default Constructor + const TextDisplay(this.directoryPath); + /// Directory path final String directoryPath; - /// Default Constructor - TextDisplay(this.directoryPath); - @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Selected Directory'), + title: const Text('Selected Directory'), content: Scrollbar( child: SingleChildScrollView( child: Text(directoryPath), ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart index ab0b5c32187c..c598cbdf2611 100644 --- a/packages/file_selector/file_selector/example/lib/home_page.dart +++ b/packages/file_selector/file_selector/example/lib/home_page.dart @@ -14,7 +14,7 @@ class HomePage extends StatelessWidget { ); return Scaffold( appBar: AppBar( - title: Text('File Selector Demo Home Page'), + title: const Text('File Selector Demo Home Page'), ), body: Center( child: Column( @@ -22,31 +22,31 @@ class HomePage extends StatelessWidget { children: [ ElevatedButton( style: style, - child: Text('Open a text file'), + child: const Text('Open a text file'), onPressed: () => Navigator.pushNamed(context, '/open/text'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open an image'), + child: const Text('Open an image'), onPressed: () => Navigator.pushNamed(context, '/open/image'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open multiple images'), + child: const Text('Open multiple images'), onPressed: () => Navigator.pushNamed(context, '/open/images'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Save a file'), + child: const Text('Save a file'), onPressed: () => Navigator.pushNamed(context, '/save/text'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open a get directory dialog'), + child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), ], diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart index bd4c9b7ab853..14ce3f593f33 100644 --- a/packages/file_selector/file_selector/example/lib/main.dart +++ b/packages/file_selector/file_selector/example/lib/main.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/material.dart'; -import 'package:example/home_page.dart'; import 'package:example/get_directory_page.dart'; -import 'package:example/open_text_page.dart'; +import 'package:example/home_page.dart'; import 'package:example/open_image_page.dart'; import 'package:example/open_multiple_images_page.dart'; +import 'package:example/open_text_page.dart'; import 'package:example/save_text_page.dart'; +import 'package:flutter/material.dart'; void main() { runApp(MyApp()); @@ -25,12 +25,12 @@ class MyApp extends StatelessWidget { visualDensity: VisualDensity.adaptivePlatformDensity, ), home: HomePage(), - routes: { - '/open/image': (context) => OpenImagePage(), - '/open/images': (context) => OpenMultipleImagesPage(), - '/open/text': (context) => OpenTextPage(), - '/save/text': (context) => SaveTextPage(), - '/directory': (context) => GetDirectoryPage(), + routes: { + '/open/image': (BuildContext context) => OpenImagePage(), + '/open/images': (BuildContext context) => OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => OpenTextPage(), + '/save/text': (BuildContext context) => SaveTextPage(), + '/directory': (BuildContext context) => GetDirectoryPage(), }, ); } diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index ae0d5ad4eefb..0abdba6eb72d 100644 --- a/packages/file_selector/file_selector/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -3,18 +3,20 @@ // found in the LICENSE file. import 'dart:io'; -import 'package:flutter/foundation.dart'; + import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenImagePage extends StatelessWidget { - void _openImageFile(BuildContext context) async { + Future _openImageFile(BuildContext context) async { final XTypeGroup typeGroup = XTypeGroup( label: 'images', - extensions: ['jpg', 'png'], + extensions: ['jpg', 'png'], ); - final List files = await openFiles(acceptedTypeGroups: [typeGroup]); + final List files = + await openFiles(acceptedTypeGroups: [typeGroup]); if (files.isEmpty) { // Operation was canceled by the user. return; @@ -23,9 +25,9 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( + await showDialog( context: context, - builder: (context) => ImageDisplay(fileName, filePath), + builder: (BuildContext context) => ImageDisplay(fileName, filePath), ); } @@ -33,7 +35,7 @@ class OpenImagePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open an image"), + title: const Text('Open an image'), ), body: Center( child: Column( @@ -44,7 +46,7 @@ class OpenImagePage extends StatelessWidget { primary: Colors.blue, onPrimary: Colors.white, ), - child: Text('Press to open an image file(png, jpg)'), + child: const Text('Press to open an image file(png, jpg)'), onPressed: () => _openImageFile(context), ), ], @@ -56,15 +58,15 @@ class OpenImagePage extends StatelessWidget { /// Widget that displays a text file in a dialog class ImageDisplay extends StatelessWidget { + /// Default Constructor + const ImageDisplay(this.fileName, this.filePath); + /// Image's name final String fileName; /// Image's path final String filePath; - /// Default Constructor - ImageDisplay(this.fileName, this.filePath); - @override Widget build(BuildContext context) { return AlertDialog( @@ -72,7 +74,7 @@ class ImageDisplay extends StatelessWidget { // On web the filePath is a blob url // while on other platforms it is a system path. content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () { diff --git a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart index 5bf080eff450..9a1101214aaa 100644 --- a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -3,22 +3,23 @@ // found in the LICENSE file. import 'dart:io'; -import 'package:flutter/foundation.dart'; + import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenMultipleImagesPage extends StatelessWidget { - void _openImageFile(BuildContext context) async { + Future _openImageFile(BuildContext context) async { final XTypeGroup jpgsTypeGroup = XTypeGroup( label: 'JPEGs', - extensions: ['jpg', 'jpeg'], + extensions: ['jpg', 'jpeg'], ); final XTypeGroup pngTypeGroup = XTypeGroup( label: 'PNGs', - extensions: ['png'], + extensions: ['png'], ); - final List files = await openFiles(acceptedTypeGroups: [ + final List files = await openFiles(acceptedTypeGroups: [ jpgsTypeGroup, pngTypeGroup, ]); @@ -26,9 +27,9 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( + await showDialog( context: context, - builder: (context) => MultipleImagesDisplay(files), + builder: (BuildContext context) => MultipleImagesDisplay(files), ); } @@ -36,7 +37,7 @@ class OpenMultipleImagesPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open multiple images"), + title: const Text('Open multiple images'), ), body: Center( child: Column( @@ -47,7 +48,7 @@ class OpenMultipleImagesPage extends StatelessWidget { primary: Colors.blue, onPrimary: Colors.white, ), - child: Text('Press to open multiple images (png, jpg)'), + child: const Text('Press to open multiple images (png, jpg)'), onPressed: () => _openImageFile(context), ), ], @@ -59,23 +60,23 @@ class OpenMultipleImagesPage extends StatelessWidget { /// Widget that displays a text file in a dialog class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor + const MultipleImagesDisplay(this.files); + /// The files containing the images final List files; - /// Default Constructor - MultipleImagesDisplay(this.files); - @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Gallery'), + title: const Text('Gallery'), // On web the filePath is a blob url // while on other platforms it is a system path. content: Center( child: Row( children: [ ...files.map( - (file) => Flexible( + (XFile file) => Flexible( child: kIsWeb ? Image.network(file.path) : Image.file(File(file.path))), @@ -83,7 +84,7 @@ class MultipleImagesDisplay extends StatelessWidget { ], ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () { diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart index 8451378f7baa..652e8596cf81 100644 --- a/packages/file_selector/file_selector/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -7,12 +7,13 @@ import 'package:flutter/material.dart'; /// Screen that shows an example of openFile class OpenTextPage extends StatelessWidget { - void _openTextFile(BuildContext context) async { + Future _openTextFile(BuildContext context) async { final XTypeGroup typeGroup = XTypeGroup( label: 'text', - extensions: ['txt', 'json'], + extensions: ['txt', 'json'], ); - final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); + final XFile? file = + await openFile(acceptedTypeGroups: [typeGroup]); if (file == null) { // Operation was canceled by the user. return; @@ -20,9 +21,9 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( + await showDialog( context: context, - builder: (context) => TextDisplay(fileName, fileContent), + builder: (BuildContext context) => TextDisplay(fileName, fileContent), ); } @@ -30,7 +31,7 @@ class OpenTextPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open a text file"), + title: const Text('Open a text file'), ), body: Center( child: Column( @@ -41,7 +42,7 @@ class OpenTextPage extends StatelessWidget { primary: Colors.blue, onPrimary: Colors.white, ), - child: Text('Press to open a text file (json, txt)'), + child: const Text('Press to open a text file (json, txt)'), onPressed: () => _openTextFile(context), ), ], @@ -53,15 +54,15 @@ class OpenTextPage extends StatelessWidget { /// Widget that displays a text file in a dialog class TextDisplay extends StatelessWidget { + /// Default Constructor + const TextDisplay(this.fileName, this.fileContent); + /// File's name final String fileName; /// File to display final String fileContent; - /// Default Constructor - TextDisplay(this.fileName, this.fileContent); - @override Widget build(BuildContext context) { return AlertDialog( @@ -71,7 +72,7 @@ class TextDisplay extends StatelessWidget { child: Text(fileContent), ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart index 1610fc05164d..108ef89b0248 100644 --- a/packages/file_selector/file_selector/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -11,8 +11,8 @@ class SaveTextPage extends StatelessWidget { final TextEditingController _nameController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); - void _saveFile() async { - String? path = await getSavePath(); + Future _saveFile() async { + final String? path = await getSavePath(); if (path == null) { // Operation was canceled by the user. return; @@ -20,7 +20,7 @@ class SaveTextPage extends StatelessWidget { final String text = _contentController.text; final String fileName = _nameController.text; final Uint8List fileData = Uint8List.fromList(text.codeUnits); - final String fileMimeType = 'text/plain'; + const String fileMimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); await textFile.saveTo(path); @@ -30,7 +30,7 @@ class SaveTextPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Save text into a file"), + title: const Text('Save text into a file'), ), body: Center( child: Column( @@ -42,7 +42,7 @@ class SaveTextPage extends StatelessWidget { minLines: 1, maxLines: 12, controller: _nameController, - decoration: InputDecoration( + decoration: const InputDecoration( hintText: '(Optional) Suggest File Name', ), ), @@ -53,18 +53,18 @@ class SaveTextPage extends StatelessWidget { minLines: 1, maxLines: 12, controller: _contentController, - decoration: InputDecoration( + decoration: const InputDecoration( hintText: 'Enter File Contents', ), ), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.blue, onPrimary: Colors.white, ), - child: Text('Press to save a text file'), + child: const Text('Press to save a text file'), onPressed: () => _saveFile(), ), ], diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml index 4987c836ad63..531f4790afd0 100644 --- a/packages/file_selector/file_selector/example/pubspec.yaml +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -8,9 +8,6 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - flutter: - sdk: flutter - file_selector: # When depending on this package from a real application you should use: # file_selector: ^x.y.z @@ -18,6 +15,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + flutter: + sdk: flutter dev_dependencies: flutter_test: 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 6d5e215eeb01..6ab0bd975036 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -2,23 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:file_selector/file_selector.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:test/fake.dart'; void main() { late FakeFileSelector fakePlatformImplementation; - final initialDirectory = '/home/flutteruser'; - final confirmButtonText = 'Use this profile picture'; - final suggestedName = 'suggested_name'; - final acceptedTypeGroups = [ - XTypeGroup(label: 'documents', mimeTypes: [ + const String initialDirectory = '/home/flutteruser'; + const String confirmButtonText = 'Use this profile picture'; + const String suggestedName = 'suggested_name'; + final List acceptedTypeGroups = [ + XTypeGroup(label: 'documents', mimeTypes: [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessing', ]), - XTypeGroup(label: 'images', extensions: [ + XTypeGroup(label: 'images', extensions: [ 'jpg', 'png', ]), @@ -30,7 +30,7 @@ void main() { }); group('openFile', () { - final expectedFile = XFile('path'); + final XFile expectedFile = XFile('path'); test('works', () async { fakePlatformImplementation @@ -40,7 +40,7 @@ void main() { acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse([expectedFile]); - final file = await openFile( + final XFile? file = await openFile( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, @@ -52,7 +52,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setFileResponse([expectedFile]); - final file = await openFile(); + final XFile? file = await openFile(); expect(file, expectedFile); }); @@ -62,7 +62,7 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setFileResponse([expectedFile]); - final file = await openFile(initialDirectory: initialDirectory); + final XFile? file = await openFile(initialDirectory: initialDirectory); expect(file, expectedFile); }); @@ -71,7 +71,7 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setFileResponse([expectedFile]); - final file = await openFile(confirmButtonText: confirmButtonText); + final XFile? file = await openFile(confirmButtonText: confirmButtonText); expect(file, expectedFile); }); @@ -80,13 +80,14 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse([expectedFile]); - final file = await openFile(acceptedTypeGroups: acceptedTypeGroups); + final XFile? file = + await openFile(acceptedTypeGroups: acceptedTypeGroups); expect(file, expectedFile); }); }); group('openFiles', () { - final expectedFiles = [XFile('path')]; + final List expectedFiles = [XFile('path')]; test('works', () async { fakePlatformImplementation @@ -96,19 +97,19 @@ void main() { acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse(expectedFiles); - final file = await openFiles( + final List files = await openFiles( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, ); - expect(file, expectedFiles); + expect(files, expectedFiles); }); test('works with no arguments', () async { fakePlatformImplementation.setFileResponse(expectedFiles); - final files = await openFiles(); + final List files = await openFiles(); expect(files, expectedFiles); }); @@ -118,7 +119,8 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setFileResponse(expectedFiles); - final files = await openFiles(initialDirectory: initialDirectory); + final List files = + await openFiles(initialDirectory: initialDirectory); expect(files, expectedFiles); }); @@ -127,7 +129,8 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setFileResponse(expectedFiles); - final files = await openFiles(confirmButtonText: confirmButtonText); + final List files = + await openFiles(confirmButtonText: confirmButtonText); expect(files, expectedFiles); }); @@ -136,13 +139,14 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse(expectedFiles); - final files = await openFiles(acceptedTypeGroups: acceptedTypeGroups); + final List files = + await openFiles(acceptedTypeGroups: acceptedTypeGroups); expect(files, expectedFiles); }); }); group('getSavePath', () { - final expectedSavePath = '/example/path'; + const String expectedSavePath = '/example/path'; test('works', () async { fakePlatformImplementation @@ -153,7 +157,7 @@ void main() { suggestedName: suggestedName) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath( + final String? savePath = await getSavePath( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, @@ -166,7 +170,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setPathResponse(expectedSavePath); - final savePath = await getSavePath(); + final String? savePath = await getSavePath(); expect(savePath, expectedSavePath); }); @@ -175,7 +179,8 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(initialDirectory: initialDirectory); + final String? savePath = + await getSavePath(initialDirectory: initialDirectory); expect(savePath, expectedSavePath); }); @@ -184,7 +189,8 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(confirmButtonText: confirmButtonText); + final String? savePath = + await getSavePath(confirmButtonText: confirmButtonText); expect(savePath, expectedSavePath); }); @@ -193,7 +199,7 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setPathResponse(expectedSavePath); - final savePath = + final String? savePath = await getSavePath(acceptedTypeGroups: acceptedTypeGroups); expect(savePath, expectedSavePath); }); @@ -203,13 +209,13 @@ void main() { ..setExpectations(suggestedName: suggestedName) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(suggestedName: suggestedName); + final String? savePath = await getSavePath(suggestedName: suggestedName); expect(savePath, expectedSavePath); }); }); group('getDirectoryPath', () { - final expectedDirectoryPath = '/example/path'; + const String expectedDirectoryPath = '/example/path'; test('works', () async { fakePlatformImplementation @@ -218,7 +224,7 @@ void main() { confirmButtonText: confirmButtonText) ..setPathResponse(expectedDirectoryPath); - final directoryPath = await getDirectoryPath( + final String? directoryPath = await getDirectoryPath( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, ); @@ -229,7 +235,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setPathResponse(expectedDirectoryPath); - final directoryPath = await getDirectoryPath(); + final String? directoryPath = await getDirectoryPath(); expect(directoryPath, expectedDirectoryPath); }); @@ -238,7 +244,7 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setPathResponse(expectedDirectoryPath); - final directoryPath = + final String? directoryPath = await getDirectoryPath(initialDirectory: initialDirectory); expect(directoryPath, expectedDirectoryPath); }); @@ -248,7 +254,7 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setPathResponse(expectedDirectoryPath); - final directoryPath = + final String? directoryPath = await getDirectoryPath(confirmButtonText: confirmButtonText); expect(directoryPath, expectedDirectoryPath); }); @@ -295,7 +301,7 @@ class FakeFileSelector extends Fake }) async { expect(acceptedTypeGroups, this.acceptedTypeGroups); expect(initialDirectory, this.initialDirectory); - expect(suggestedName, this.suggestedName); + expect(suggestedName, suggestedName); return files?[0]; } @@ -307,7 +313,7 @@ class FakeFileSelector extends Fake }) async { expect(acceptedTypeGroups, this.acceptedTypeGroups); expect(initialDirectory, this.initialDirectory); - expect(suggestedName, this.suggestedName); + expect(suggestedName, suggestedName); return files!; } diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index ba595addfcaf..95701504fed5 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 + +* Minor code cleanup for new analysis rules. + ## 2.0.2 * Update platform_plugin_interface version requirement. 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 34017acc90e0..28ec41db6dde 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 @@ -3,9 +3,8 @@ // found in the LICENSE file. import 'package:cross_file/cross_file.dart'; -import 'package:flutter/services.dart'; - import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; const MethodChannel _channel = @@ -27,8 +26,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { final List? path = await _channel.invokeListMethod( 'openFile', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': false, @@ -47,14 +47,15 @@ class MethodChannelFileSelector extends FileSelectorPlatform { final List? pathList = await _channel.invokeListMethod( 'openFile', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': true, }, ); - return pathList?.map((path) => XFile(path)).toList() ?? []; + return pathList?.map((String path) => XFile(path)).toList() ?? []; } /// Gets the path from a save dialog @@ -68,8 +69,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { return _channel.invokeMethod( 'getSavePath', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'suggestedName': suggestedName, 'confirmButtonText': confirmButtonText, diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart index 3e3326379610..2146131023e1 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart @@ -14,7 +14,7 @@ class XTypeGroup { this.mimeTypes, this.macUTIs, this.webWildCards, - }) : this.extensions = _removeLeadingDots(extensions); + }) : extensions = _removeLeadingDots(extensions); /// The 'name' or reference to this group of types final String? label; @@ -42,6 +42,7 @@ class XTypeGroup { }; } - static List? _removeLeadingDots(List? exts) => - exts?.map((ext) => ext.startsWith('.') ? ext.substring(1) : ext).toList(); + static List? _removeLeadingDots(List? exts) => exts + ?.map((String ext) => ext.startsWith('.') ? ext.substring(1) : ext) + .toList(); } diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart index 0f157ed0be5a..bc7136f80bd6 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart @@ -6,7 +6,7 @@ import 'dart:html'; /// Create anchor element with download attribute AnchorElement createAnchorElement(String href, String? suggestedName) { - final element = AnchorElement(href: href); + final AnchorElement element = AnchorElement(href: href); if (suggestedName == null) { element.download = 'download'; @@ -27,7 +27,7 @@ void addElementToContainerAndClick(Element container, Element element) { /// Initializes a DOM container where we can host elements. Element ensureInitialized(String id) { - var target = querySelector('#${id}'); + Element? target = querySelector('#$id'); if (target == null) { final Element targetElement = Element.tag('flt-x-file')..id = id; diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index f5b609c93ed3..a5b22b977d69 100644 --- a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; - import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('$FileSelectorPlatform', () { diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart index 96a3c2d4f4c9..33f9fbf45a8b 100644 --- a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart @@ -2,17 +2,16 @@ // 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:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelFileSelector()', () { - MethodChannelFileSelector plugin = MethodChannelFileSelector(); + final MethodChannelFileSelector plugin = MethodChannelFileSelector(); final List log = []; @@ -27,27 +26,31 @@ void main() { group('#openFile', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + await plugin + .openFile(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'confirmButtonText': null, 'multiple': false, @@ -56,14 +59,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.openFile(initialDirectory: "/example/directory"); + await plugin.openFile(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, 'multiple': false, }), @@ -71,7 +74,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.openFile(confirmButtonText: "Open File"); + await plugin.openFile(confirmButtonText: 'Open File'); expect( log, @@ -79,7 +82,7 @@ void main() { isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', 'multiple': false, }), ], @@ -88,27 +91,31 @@ void main() { }); group('#openFiles', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + await plugin + .openFiles(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'confirmButtonText': null, 'multiple': true, @@ -117,14 +124,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.openFiles(initialDirectory: "/example/directory"); + await plugin.openFiles(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, 'multiple': true, }), @@ -132,7 +139,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.openFiles(confirmButtonText: "Open File"); + await plugin.openFiles(confirmButtonText: 'Open File'); expect( log, @@ -140,7 +147,7 @@ void main() { isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', 'multiple': true, }), ], @@ -150,27 +157,31 @@ void main() { group('#getSavePath', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.getSavePath(acceptedTypeGroups: [group, groupTwo]); + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'suggestedName': null, 'confirmButtonText': null, @@ -179,14 +190,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.getSavePath(initialDirectory: "/example/directory"); + await plugin.getSavePath(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('getSavePath', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'suggestedName': null, 'confirmButtonText': null, }), @@ -194,7 +205,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.getSavePath(confirmButtonText: "Open File"); + await plugin.getSavePath(confirmButtonText: 'Open File'); expect( log, @@ -203,34 +214,34 @@ void main() { 'acceptedTypeGroups': null, 'initialDirectory': null, 'suggestedName': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', }), ], ); }); group('#getDirectoryPath', () { test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: "/example/directory"); + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, }), ], ); }); test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: "Open File"); + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); expect( log, [ isMethodCall('getDirectoryPath', arguments: { 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', }), ], ); diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart index e85a4929e411..84f5ca1f0bd2 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart @@ -2,19 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('XTypeGroup', () { test('toJSON() creates correct map', () { - final label = 'test group'; - final extensions = ['txt', 'jpg']; - final mimeTypes = ['text/plain']; - final macUTIs = ['public.plain-text']; - final webWildCards = ['image/*']; + const String label = 'test group'; + final List extensions = ['txt', 'jpg']; + final List mimeTypes = ['text/plain']; + final List macUTIs = ['public.plain-text']; + final List webWildCards = ['image/*']; - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: label, extensions: extensions, mimeTypes: mimeTypes, @@ -22,7 +22,7 @@ void main() { webWildCards: webWildCards, ); - final jsonMap = group.toJSON(); + final Map jsonMap = group.toJSON(); expect(jsonMap['label'], label); expect(jsonMap['extensions'], extensions); expect(jsonMap['mimeTypes'], mimeTypes); @@ -31,11 +31,11 @@ void main() { }); test('A wildcard group can be created', () { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'Any', ); - final jsonMap = group.toJSON(); + final Map jsonMap = group.toJSON(); expect(jsonMap['extensions'], null); expect(jsonMap['mimeTypes'], null); expect(jsonMap['macUTIs'], null); @@ -43,10 +43,10 @@ void main() { }); test('Leading dots are removed from extensions', () { - final extensions = ['.txt', '.jpg']; - final group = XTypeGroup(extensions: extensions); + final List extensions = ['.txt', '.jpg']; + final XTypeGroup group = XTypeGroup(extensions: extensions); - expect(group.extensions, ['txt', 'jpg']); + expect(group.extensions, ['txt', 'jpg']); }); }); } diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index e2a863643027..dabd7173868c 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Minor code cleanup for new analysis rules. + ## 0.8.1+2 * Add `implements` to pubspec. diff --git a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart index f000574861ab..ee1af8cb62fd 100644 --- a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart +++ b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart @@ -3,10 +3,11 @@ // found in the LICENSE file. import 'dart:html'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:file_selector_web/src/dom_helper.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; void main() { group('dom_helper', () { @@ -15,7 +16,7 @@ void main() { late FileUploadInputElement input; FileList? createFileList(List files) { - final dataTransfer = DataTransfer(); + final DataTransfer dataTransfer = DataTransfer(); files.forEach(dataTransfer.items!.add); return dataTransfer.files as FileList?; } @@ -31,15 +32,15 @@ void main() { }); group('getFiles', () { - final mockFile1 = File(['123456'], 'file1.txt'); - final mockFile2 = File([], 'file2.txt'); + final File mockFile1 = File(['123456'], 'file1.txt'); + final File mockFile2 = File([], 'file2.txt'); testWidgets('works', (_) async { final Future> futureFiles = domHelper.getFiles( input: input, ); - setFilesAndTriggerChange([mockFile1, mockFile2]); + setFilesAndTriggerChange([mockFile1, mockFile2]); final List files = await futureFiles; @@ -62,7 +63,7 @@ void main() { // It should work the first time futureFiles = domHelper.getFiles(input: input); - setFilesAndTriggerChange([mockFile1]); + setFilesAndTriggerChange([mockFile1]); files = await futureFiles; @@ -71,7 +72,7 @@ void main() { // The same input should work more than once futureFiles = domHelper.getFiles(input: input); - setFilesAndTriggerChange([mockFile2]); + setFilesAndTriggerChange([mockFile2]); files = await futureFiles; @@ -80,14 +81,14 @@ void main() { }); testWidgets('sets the attributes and clicks it', (_) async { - final accept = '.jpg,.png'; - final multiple = true; + const String accept = '.jpg,.png'; + const bool multiple = true; bool wasClicked = false; //ignore: unawaited_futures input.onClick.first.then((_) => wasClicked = true); - final futureFile = domHelper.getFiles( + final Future> futureFile = domHelper.getFiles( accept: accept, multiple: multiple, input: input, @@ -103,7 +104,7 @@ void main() { 'The should be clicked otherwise no dialog will be shown', ); - setFilesAndTriggerChange([]); + setFilesAndTriggerChange([]); await futureFile; // It should be already removed from the DOM after the file is resolved. diff --git a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart index c16aa1cf454e..fe57d1d1e15d 100644 --- a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart +++ b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart @@ -4,11 +4,12 @@ import 'dart:html'; import 'dart:typed_data'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_web/file_selector_web.dart'; import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { group('FileSelectorWeb', () { @@ -16,23 +17,25 @@ void main() { group('openFile', () { testWidgets('works', (WidgetTester _) async { - final mockFile = createXFile('1001', 'identity.png'); + final XFile mockFile = createXFile('1001', 'identity.png'); - final mockDomHelper = MockDomHelper() - ..setFiles([mockFile]) + final MockDomHelper mockDomHelper = MockDomHelper() + ..setFiles([mockFile]) ..expectAccept('.jpg,.jpeg,image/png,image/*') ..expectMultiple(false); - final plugin = FileSelectorWeb(domHelper: mockDomHelper); + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); - final typeGroup = XTypeGroup( + final XTypeGroup typeGroup = XTypeGroup( label: 'images', - extensions: ['jpg', 'jpeg'], - mimeTypes: ['image/png'], - webWildCards: ['image/*'], + extensions: ['jpg', 'jpeg'], + mimeTypes: ['image/png'], + webWildCards: ['image/*'], ); - final file = await plugin.openFile(acceptedTypeGroups: [typeGroup]); + final XFile file = + await plugin.openFile(acceptedTypeGroups: [typeGroup]); expect(file.name, mockFile.name); expect(await file.length(), 4); @@ -43,22 +46,24 @@ void main() { group('openFiles', () { testWidgets('works', (WidgetTester _) async { - final mockFile1 = createXFile('123456', 'file1.txt'); - final mockFile2 = createXFile('', 'file2.txt'); + final XFile mockFile1 = createXFile('123456', 'file1.txt'); + final XFile mockFile2 = createXFile('', 'file2.txt'); - final mockDomHelper = MockDomHelper() - ..setFiles([mockFile1, mockFile2]) + final MockDomHelper mockDomHelper = MockDomHelper() + ..setFiles([mockFile1, mockFile2]) ..expectAccept('.txt') ..expectMultiple(true); - final plugin = FileSelectorWeb(domHelper: mockDomHelper); + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); - final typeGroup = XTypeGroup( + final XTypeGroup typeGroup = XTypeGroup( label: 'files', - extensions: ['.txt'], + extensions: ['.txt'], ); - final files = await plugin.openFiles(acceptedTypeGroups: [typeGroup]); + final List files = + await plugin.openFiles(acceptedTypeGroups: [typeGroup]); expect(files.length, 2); @@ -76,8 +81,8 @@ void main() { group('getSavePath', () { testWidgets('returns non-null', (WidgetTester _) async { - final plugin = FileSelectorWeb(); - final savePath = plugin.getSavePath(); + final FileSelectorWeb plugin = FileSelectorWeb(); + final Future savePath = plugin.getSavePath(); expect(await savePath, isNotNull); }); }); @@ -99,7 +104,7 @@ class MockDomHelper implements DomHelper { reason: 'Expected "accept" value does not match.'); expect(multiple, _expectedMultiple, reason: 'Expected "multiple" value does not match.'); - return Future.value(_files); + return Future>.value(_files); } void setFiles(List files) { @@ -116,6 +121,6 @@ class MockDomHelper implements DomHelper { } XFile createXFile(String content, String name) { - final data = Uint8List.fromList(content.codeUnits); + final Uint8List data = Uint8List.fromList(content.codeUnits); return XFile.fromData(data, name: name, lastModified: DateTime.now()); } diff --git a/packages/file_selector/file_selector_web/example/lib/main.dart b/packages/file_selector/file_selector_web/example/lib/main.dart index e1a38dcdcd46..341913a18490 100644 --- a/packages/file_selector/file_selector_web/example/lib/main.dart +++ b/packages/file_selector/file_selector_web/example/lib/main.dart @@ -17,7 +17,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart index f7c10b36a186..8f4ca202593e 100644 --- a/packages/file_selector/file_selector_web/lib/file_selector_web.dart +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -3,16 +3,23 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_web/src/dom_helper.dart'; import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:meta/meta.dart'; /// The web implementation of [FileSelectorPlatform]. /// /// This class implements the `package:file_selector` functionality for the web. class FileSelectorWeb extends FileSelectorPlatform { + /// Default constructor, initializes _domHelper that we can use + /// to interact with the DOM. + /// overrides parameter allows for testing to override functions + FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) + : _domHelper = domHelper ?? DomHelper(); + final DomHelper _domHelper; /// Registers this class as the default instance of [FileSelectorPlatform]. @@ -20,19 +27,14 @@ class FileSelectorWeb extends FileSelectorPlatform { FileSelectorPlatform.instance = FileSelectorWeb(); } - /// Default constructor, initializes _domHelper that we can use - /// to interact with the DOM. - /// overrides parameter allows for testing to override functions - FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) - : _domHelper = domHelper ?? DomHelper(); - @override Future openFile({ List? acceptedTypeGroups, String? initialDirectory, String? confirmButtonText, }) async { - final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups); + final List files = + await _openFiles(acceptedTypeGroups: acceptedTypeGroups); return files.first; } @@ -68,7 +70,7 @@ class FileSelectorWeb extends FileSelectorPlatform { List? acceptedTypeGroups, bool multiple = false, }) async { - final accept = acceptedTypesToString(acceptedTypeGroups); + final String accept = acceptedTypesToString(acceptedTypeGroups); return _domHelper.getFiles( accept: accept, multiple: multiple, diff --git a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart index 0d251af9cc7f..06c13d968484 100644 --- a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart +++ b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart @@ -4,27 +4,28 @@ import 'dart:async'; import 'dart:html'; -import 'package:meta/meta.dart'; -import 'package:flutter/services.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; /// Class to manipulate the DOM with the intention of reading files from it. class DomHelper { - final _container = Element.tag('file-selector'); - /// Default constructor, initializes the container DOM element. DomHelper() { - final body = querySelector('body')!; + final Element body = querySelector('body')!; body.children.add(_container); } + final Element _container = Element.tag('file-selector'); + /// Sets the attributes and waits for a file to be selected. Future> getFiles({ String accept = '', bool multiple = false, @visibleForTesting FileUploadInputElement? input, }) { - final Completer> completer = Completer(); + final Completer> completer = Completer>(); final FileUploadInputElement inputElement = input ?? FileUploadInputElement(); @@ -41,9 +42,9 @@ class DomHelper { completer.complete(files); }); - inputElement.onError.first.then((event) { + inputElement.onError.first.then((Event event) { final ErrorEvent error = event as ErrorEvent; - final platformException = PlatformException( + final PlatformException platformException = PlatformException( code: error.type, message: error.message, ); diff --git a/packages/file_selector/file_selector_web/lib/src/utils.dart b/packages/file_selector/file_selector_web/lib/src/utils.dart index e52c00d1c223..6a534645fda6 100644 --- a/packages/file_selector/file_selector_web/lib/src/utils.dart +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -6,9 +6,11 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac /// Convert list of XTypeGroups to a comma-separated string String acceptedTypesToString(List? acceptedTypes) { - if (acceptedTypes == null) return ''; - final List allTypes = []; - for (final group in acceptedTypes) { + if (acceptedTypes == null) { + return ''; + } + final List allTypes = []; + for (final XTypeGroup group in acceptedTypes) { _assertTypeGroupIsValid(group); if (group.extensions != null) { allTypes.addAll(group.extensions!.map(_normalizeExtension)); diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart index 2951af24dff9..9bddfd2e6304 100644 --- a/packages/file_selector/file_selector_web/test/utils_test.dart +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -2,54 +2,55 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_web/src/utils.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('FileSelectorWeb utils', () { group('acceptedTypesToString', () { test('works', () { - final List acceptedTypes = [ - XTypeGroup(label: 'images', webWildCards: ['images/*']), - XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), - XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['images/*']), + XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'images/*,.jpg,.jpeg,image/png'); }); test('works with an empty list', () { - final List acceptedTypes = []; - final accepts = acceptedTypesToString(acceptedTypes); + final List acceptedTypes = []; + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, ''); }); test('works with extensions', () { - final List acceptedTypes = [ - XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), - XTypeGroup(label: 'pngs', extensions: ['png']), + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), + XTypeGroup(label: 'pngs', extensions: ['png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, '.jpeg,.jpg,.png'); }); test('works with mime types', () { - final List acceptedTypes = [ - XTypeGroup(label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), - XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + final List acceptedTypes = [ + XTypeGroup( + label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'image/jpeg,image/jpg,image/png'); }); test('works with web wild cards', () { - final List acceptedTypes = [ - XTypeGroup(label: 'images', webWildCards: ['image/*']), - XTypeGroup(label: 'audios', webWildCards: ['audio/*']), - XTypeGroup(label: 'videos', webWildCards: ['video/*']), + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['image/*']), + XTypeGroup(label: 'audios', webWildCards: ['audio/*']), + XTypeGroup(label: 'videos', webWildCards: ['video/*']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'image/*,audio/*,video/*'); }); }); diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 2e214757d90d..71d8fb825add 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -16,7 +16,6 @@ # TODO(ecosystem): Remove everything from this list. See: # https://github.com/flutter/flutter/issues/76229 - camera -- file_selector - google_maps_flutter - google_sign_in - image_picker From fe31e5292f14eee3b7a9c3a7375bb04acd159057 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 29 Sep 2021 10:58:04 +0200 Subject: [PATCH 083/259] Handle restored purchases in iOS example app (#4392) --- packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 3 ++- .../in_app_purchase/in_app_purchase_ios/example/lib/main.dart | 3 ++- packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 7e3902b206ab..76cafa9201cc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.3+5 +* Updated example app to handle restored purchases properly. * Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. ## 0.1.3+4 diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart index 19884745bce8..15ab64b6ea80 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart @@ -399,7 +399,8 @@ class _MyAppState extends State<_MyApp> { } else { if (purchaseDetails.status == PurchaseStatus.error) { handleError(purchaseDetails.error!); - } else if (purchaseDetails.status == PurchaseStatus.purchased) { + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 30a57bb56c94..fdd769e90674 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+4 +version: 0.1.3+5 environment: sdk: ">=2.14.0 <3.0.0" From d9a4b753e7b41595fc9b7ebeee5ef1d65c9ad07d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 29 Sep 2021 13:08:07 +0200 Subject: [PATCH 084/259] Handle `PurchaseStatus.restored` correctly in example. (#4393) --- packages/in_app_purchase/in_app_purchase/CHANGELOG.md | 5 +++++ .../in_app_purchase/in_app_purchase/example/lib/main.dart | 3 ++- packages/in_app_purchase/in_app_purchase/pubspec.yaml | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 95ba4f27d10a..859d0bb6432f 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 @@ +## 1.0.9 + +* Handle purchases with `PurchaseStatus.restored` correctly in the example App. +* Updated dependencies on `in_app_purchase_android` and `in_app_purchase_ios` to their latest versions (version 0.1.5 and 0.1.3+5 respectively). + ## 1.0.8 * Fix repository link in pubspec.yaml. diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart index 73ecadb3f15d..3cf7229fdbc2 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -429,7 +429,8 @@ class _MyAppState extends State<_MyApp> { } else { if (purchaseDetails.status == PurchaseStatus.error) { handleError(purchaseDetails.error!); - } else if (purchaseDetails.status == PurchaseStatus.purchased) { + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 8b4510b3fce4..96570f7aa168 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.8 +version: 1.0.9 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,8 +20,8 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.0.0 - in_app_purchase_android: ^0.1.4 - in_app_purchase_ios: ^0.1.1 + in_app_purchase_android: ^0.1.5 + in_app_purchase_ios: ^0.1.3+5 dev_dependencies: flutter_driver: From a1304efe49e92da6999a765dbbdddd04e595ba29 Mon Sep 17 00:00:00 2001 From: Aman Verma Date: Thu, 30 Sep 2021 00:38:03 +0530 Subject: [PATCH 085/259] [webview_flutter] Fixed todos in FlutterWebView.java (#4394) --- .../webview_flutter_android/CHANGELOG.md | 4 +++ .../webviewflutter/FlutterWebView.java | 28 +++---------------- .../webview_flutter_android/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 917a3c73acb1..d4827a71e47d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.15 + +* Added Overrides in FlutterWebView.java + ## 2.0.14 * Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index 4651a5f5ae22..ff573c771960 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -188,48 +188,28 @@ public View getView() { return webView; } - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + @Override public void onInputConnectionUnlocked() { if (webView instanceof InputAwareWebView) { ((InputAwareWebView) webView).unlockInputConnection(); } } - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + @Override public void onInputConnectionLocked() { if (webView instanceof InputAwareWebView) { ((InputAwareWebView) webView).lockInputConnection(); } } - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + @Override public void onFlutterViewAttached(View flutterView) { if (webView instanceof InputAwareWebView) { ((InputAwareWebView) webView).setContainerView(flutterView); } } - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + @Override public void onFlutterViewDetached() { if (webView instanceof InputAwareWebView) { ((InputAwareWebView) webView).setContainerView(null); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 4a3d4cb1d942..36f186087c08 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.14 +version: 2.0.15 environment: sdk: ">=2.14.0 <3.0.0" From 90b2844ce7e58c2c5466303f8d6e8adf031d0f19 Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Thu, 30 Sep 2021 14:25:34 +1300 Subject: [PATCH 086/259] [google_maps_flutter_web] Add Marker drag events (#4385) --- .../google_maps_flutter_web/CHANGELOG.md | 4 +++ .../google_maps_controller_test.mocks.dart | 7 ++--- .../google_maps_plugin_test.dart | 22 +++++++++++++++ .../google_maps_plugin_test.mocks.dart | 7 ++--- .../example/integration_test/marker_test.dart | 28 +++++++++++++++++++ .../example/pubspec.yaml | 2 +- .../lib/src/google_maps_flutter_web.dart | 10 +++++++ .../lib/src/marker.dart | 18 ++++++++++++ .../lib/src/markers.dart | 22 +++++++++++++++ .../google_maps_flutter_web/pubspec.yaml | 4 +-- 10 files changed, 111 insertions(+), 13 deletions(-) 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 4d7ecf74e098..b2fe086f5c9d 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 @@ +## 0.3.2 + +Add `onDragStart` and `onDrag` to `Marker` + ## 0.3.1 * Fix the `getScreenCoordinate(LatLng)` method. [#80710](https://github.com/flutter/flutter/issues/80710) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index af8ed5420a0c..530707c6c328 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -1,8 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Mocks generated by Mockito 5.0.15 from annotations +// Mocks generated by Mockito 5.0.16 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. @@ -19,6 +15,7 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types class _FakeGMap_0 extends _i1.Fake implements _i2.GMap {} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart index 758294f5bb91..a3cf86e593fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -404,6 +404,28 @@ void main() { await _testStreamFiltering(stream, event); }); + testWidgets('onMarkerDragStart', (WidgetTester tester) async { + final event = MarkerDragStartEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDragStart(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDrag', (WidgetTester tester) async { + final event = MarkerDragEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDrag(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); testWidgets('onMarkerDragEnd', (WidgetTester tester) async { final event = MarkerDragEndEvent( mapId, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 01908ce777e7..d2df11c6ffa9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -1,8 +1,4 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Mocks generated by Mockito 5.0.15 from annotations +// Mocks generated by Mockito 5.0.16 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. @@ -20,6 +16,7 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types class _FakeStreamController_0 extends _i1.Fake implements _i2.StreamController {} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart index 2bfa27b73a77..cfa36febbbfe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -27,6 +27,14 @@ void main() { _methodCalledCompleter.complete(true); } + void onDragStart(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + + void onDrag(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + void onDragEnd(gmaps.LatLng _) { _methodCalledCompleter.complete(true); } @@ -53,6 +61,26 @@ void main() { expect(await methodCalled, isTrue); }); + testWidgets('onDragStart gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDragStart: onDragStart); + + // Trigger a drag end event... + gmaps.Event.trigger(marker, 'dragstart', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDrag gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDrag: onDrag); + + // Trigger a drag end event... + gmaps.Event.trigger( + marker, 'drag', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + expect(await methodCalled, isTrue); + }); + testWidgets('onDragEnd gets called', (WidgetTester tester) async { MarkerController(marker: marker, onDragEnd: onDragEnd); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 249b893d198c..95a3d4253440 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none # Tests require flutter beta or greater to run. environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.1.0" + flutter: ">=2.1.0" dependencies: google_maps_flutter_web: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index d03dec93ce3f..47bfdc7bba15 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -240,6 +240,16 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _events(mapId).whereType(); } + @override + Stream onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + @override Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index 5b0169b565e5..c4cd40f43323 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -19,6 +19,8 @@ class MarkerController { required gmaps.Marker marker, gmaps.InfoWindow? infoWindow, bool consumeTapEvents = false, + LatLngCallback? onDragStart, + LatLngCallback? onDrag, LatLngCallback? onDragEnd, ui.VoidCallback? onTap, }) : _marker = marker, @@ -29,6 +31,22 @@ class MarkerController { onTap.call(); }); } + if (onDragStart != null) { + marker.onDragstart.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDrag != null) { + marker.onDrag.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }); + } if (onDragEnd != null) { marker.onDragend.listen((event) { if (marker != null) { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index b650b9bcf1c8..542a48bcb707 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -62,6 +62,12 @@ class MarkersController extends GeometryController { this.showMarkerInfoWindow(marker.markerId); _onMarkerTap(marker.markerId); }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, onDragEnd: (gmaps.LatLng latLng) { _onMarkerDragEnd(marker.markerId, latLng); }, @@ -140,6 +146,22 @@ class MarkersController extends GeometryController { _streamController.add(InfoWindowTapEvent(mapId, markerId)); } + void _onMarkerDragStart(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragStartEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + + void _onMarkerDrag(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) { _streamController.add(MarkerDragEndEvent( mapId, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 08eda25352c8..1f5fe4d96ccc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.1 +version: 0.3.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - google_maps_flutter_platform_interface: ^2.0.1 + google_maps_flutter_platform_interface: ^2.1.2 google_maps: ^5.2.0 meta: ^1.3.0 sanitize_html: ^2.0.0 From 5e54355afd23c4acb551b4c8aa630c4a7edb061d Mon Sep 17 00:00:00 2001 From: ryoheiudagawa1995 <63101140+ryoheiudagawa1995@users.noreply.github.com> Date: Thu, 30 Sep 2021 23:28:07 +0900 Subject: [PATCH 087/259] [in_app_purchase] Fix in_app_purchase_android/README.md (#4363) --- packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md | 3 +++ packages/in_app_purchase/in_app_purchase_android/README.md | 2 +- packages/in_app_purchase/in_app_purchase_android/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) 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 0c75ae3cf819..a01eb9f8b70a 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,6 @@ +## 0.1.5+1 + +* Fix a broken link in the README. ## 0.1.5 * Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field. diff --git a/packages/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md index dcf5256e3bbc..d64fbfb8c49a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -24,6 +24,6 @@ If you would like to contribute to the plugin, check out our [contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). -[1]: ../in_app_purchase/in_app_purchase +[1]: https://pub.dev/packages/in_app_purchase [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://pub.dev/packages/in_app_purchase_android/install diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 33f51cc70feb..d51abbb43edc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.5 +version: 0.1.5+1 environment: sdk: ">=2.12.0 <3.0.0" From 63eb67532a7a244b3c99a2b2f837f71441116920 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 30 Sep 2021 11:33:05 -0400 Subject: [PATCH 088/259] Add file_selector to the repo list (#4395) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d8f4f502e17..b5b688ffbaef 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ These are the available plugins in this repository. |--------|-----|--------|------------|-------| | [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) | [![pub points](https://badges.bar/camera/pub%20points)](https://pub.dev/packages/camera/score) | [![popularity](https://badges.bar/camera/popularity)](https://pub.dev/packages/camera/score) | [![likes](https://badges.bar/camera/likes)](https://pub.dev/packages/camera/score) | | [espresso](./packages/espresso/) | [![pub package](https://img.shields.io/pub/v/espresso.svg)](https://pub.dev/packages/espresso) | [![pub points](https://badges.bar/espresso/pub%20points)](https://pub.dev/packages/espresso/score) | [![popularity](https://badges.bar/espresso/popularity)](https://pub.dev/packages/espresso/score) | [![likes](https://badges.bar/espresso/likes)](https://pub.dev/packages/espresso/score) | +| [file_selector](./packages/file_selector/) | [![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dev/packages/file_selector) | [![pub points](https://badges.bar/file_selector/pub%20points)](https://pub.dev/packages/file_selector/score) | [![popularity](https://badges.bar/file_selector/popularity)](https://pub.dev/packages/file_selector/score) | [![likes](https://badges.bar/file_selector/likes)](https://pub.dev/packages/file_selector/score) | | [flutter_plugin_android_lifecycle](./packages/flutter_plugin_android_lifecycle/) | [![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) | [![pub points](https://badges.bar/flutter_plugin_android_lifecycle/pub%20points)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![popularity](https://badges.bar/flutter_plugin_android_lifecycle/popularity)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![likes](https://badges.bar/flutter_plugin_android_lifecycle/likes)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | | [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | [![pub points](https://badges.bar/google_maps_flutter/pub%20points)](https://pub.dev/packages/google_maps_flutter/score) | [![popularity](https://badges.bar/google_maps_flutter/popularity)](https://pub.dev/packages/google_maps_flutter/score) | [![likes](https://badges.bar/google_maps_flutter/likes)](https://pub.dev/packages/google_maps_flutter/score) | | [google_sign_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | [![pub points](https://badges.bar/google_sign_in/pub%20points)](https://pub.dev/packages/google_sign_in/score) | [![popularity](https://badges.bar/google_sign_in/popularity)](https://pub.dev/packages/google_sign_in/score) | [![likes](https://badges.bar/google_sign_in/likes)](https://pub.dev/packages/google_sign_in/score) | From cf40966ef1dcb7bbc0a0e1f83cd31c017daf1ea2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 30 Sep 2021 13:30:26 -0400 Subject: [PATCH 089/259] [flutter_plugin_tools] Validate pubspec description (#4396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pub.dev deducts points for having a pubspec.yaml `description` that is too short or too long; several of our plugins are losing points on this. To ensure that we are following—and modeling—best practices, this adds a check that our `description` fields meet pub.dev expectations. Fixes our existing violations. Two are not published even though this only takes effect once published: - camera: We change this plugin pretty frequently, so this should go out soon without adding a release just for this trivial issue. - wifi_info_flutter: This is deprecated, so we don't plan to release it. It has to be fixed to allow the tool change to land though. --- packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/pubspec.yaml | 6 +- packages/espresso/CHANGELOG.md | 3 +- packages/espresso/pubspec.yaml | 3 +- .../file_selector/file_selector/CHANGELOG.md | 5 +- .../file_selector/file_selector/pubspec.yaml | 5 +- .../plugin_platform_interface/CHANGELOG.md | 4 + .../plugin_platform_interface/pubspec.yaml | 5 +- .../wifi_info_flutter/CHANGELOG.md | 1 + .../wifi_info_flutter/pubspec.yaml | 2 +- script/tool/CHANGELOG.md | 2 + .../lib/src/common/repository_package.dart | 9 ++ .../tool/lib/src/pubspec_check_command.dart | 33 +++++++ .../test/common/repository_package_test.dart | 35 +++++++ .../tool/test/pubspec_check_command_test.dart | 93 +++++++++++++++++++ 15 files changed, 198 insertions(+), 12 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index b2dda9a52436..c9dfc63eb46c 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated package description. + ## 0.9.4+1 * Fixed Android implementation throwing IllegalStateException when switching to a different activity. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 5c225eaee48f..21892b213781 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -1,7 +1,7 @@ name: camera -description: A Flutter plugin for getting information about and controlling the - camera on Android, iOS and Web. Supports previewing the camera feed, capturing images, capturing video, - and streaming image buffers to dart. +description: A Flutter plugin for controlling the camera. Supports previewing + the camera feed, capturing images and video, and streaming image buffers to + Dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 version: 0.9.4+1 diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index e00ea7065ce0..88976c88b668 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.1.0+4 * Updated Android lint settings. +* Updated package description. ## 0.1.0+3 diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 6295c0ce9694..c0f3b00d556c 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -1,8 +1,9 @@ name: espresso 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/master/packages/espresso issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+espresso%22 -version: 0.1.0+3 +version: 0.1.0+4 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 225f601c3ee6..f34ed78d4e7f 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,10 +1,11 @@ -## NEXT +## 0.8.2+1 * Minor code cleanup for new analysis rules. +* Updated package description. ## 0.8.2 -* Update platform_plugin_interface version requirement. +* Update `platform_plugin_interface` version requirement. ## 0.8.1 diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index e1725ef05b93..d69217f208ef 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -1,8 +1,9 @@ name: file_selector -description: Flutter plugin for opening and saving files. +description: Flutter plugin for opening and saving files, or selecting + directories, using native file selection UI. repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.2 +version: 0.8.2+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 987049c55996..af79d119c5f6 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Update package description. + ## 2.0.1 * Fix `federated flutter plugins` link in the README.md. diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 66527bc58a61..0b4b1782b526 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -1,5 +1,6 @@ name: plugin_platform_interface -description: Reusable base class for Flutter plugin platform interfaces. +description: Reusable base class for platform interfaces of Flutter federated + plugins, to help enforce best practices. repository: https://github.com/flutter/plugins/tree/master/packages/plugin_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+plugin_platform_interface%22 @@ -14,7 +15,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ # be done when absolutely necessary and after the ecosystem has already migrated to 1.X.Y version # that is forward compatible with 2.0.0 (ideally the ecosystem have migrated to depend on: # `plugin_platform_interface: >=1.X.Y <3.0.0`). -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md index 86f3f67af103..3d5599743710 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updated Android lint settings. +* Updated package description. ## 2.0.2 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml index cbda364aa35e..b1e1e756c633 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: wifi_info_flutter -description: A new flutter plugin project. +description: A Flutter plugin to get WiFi information such as connection status and network identifiers. repository: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+wifi_info_flutter%22 version: 2.0.2 diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 6119545260aa..2e6404e2cee4 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -10,6 +10,8 @@ major version change restriction. - Improved error handling and error messages in CHANGELOG version checks. - `license-check` now validates Kotlin files. +- `pubspec-check` now checks that the description is of the pub-recommended + length. ## 0.7.1 diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index cb586afb4dfe..3b4417ac8182 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -58,6 +58,15 @@ class RepositoryPackage { bool get isPlatformInterface => directory.basename.endsWith('_platform_interface'); + /// True if this appears to be a platform implementation package, according to + /// repository conventions. + bool get isPlatformImplementation => + // Any part of a federated plugin that isn't the platform interface and + // isn't the app-facing package should be an implementation package. + isFederated && + !isPlatformInterface && + directory.basename != directory.parent.basename; + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index fec0dcef9ac7..b99f5af68c45 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -126,6 +126,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + // Don't check descriptions for federated package components other than + // the app-facing package, since they are unlisted, and are expected to + // have short descriptions. + if (!package.isPlatformInterface && !package.isPlatformImplementation) { + final String? descriptionError = + _checkDescription(pubspec, package: package); + if (descriptionError != null) { + printError('$indentation$descriptionError'); + passing = false; + } + } } return passing; @@ -180,6 +192,27 @@ class PubspecCheckCommand extends PackageLoopingCommand { return errorMessages; } + // Validates the "description" field for a package, returning an error + // string if there are any issues. + String? _checkDescription( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final String? description = pubspec.description; + if (description == null) { + return 'Missing "description"'; + } + + if (description.length < 60) { + return '"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + if (description.length > 180) { + return '"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + } + bool _checkIssueLink(Pubspec pubspec) { return pubspec.issueTracker ?.toString() diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 5c5624312f51..4c20389ae4be 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -120,4 +120,39 @@ void main() { plugin.childDirectory('example').childDirectory('example2').path); }); }); + + group('federated plugin queries', () { + test('all return false for a simple plugin', () { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + expect(RepositoryPackage(plugin).isFederated, false); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isFederated, false); + }); + + test('handle app-facing packages', () { + final Directory plugin = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform interface packages', () { + final Directory plugin = createFakePlugin('a_plugin_platform_interface', + packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, true); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform implementation packages', () { + // A platform interface can end with anything, not just one of the known + // platform names, because of cases like webview_flutter_wkwebview. + final Directory plugin = createFakePlugin( + 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, true); + }); + }); } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 948136993d18..d09dcebce4af 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -56,6 +56,7 @@ void main() { bool includeHomepage = false, bool includeIssueTracker = true, bool publishable = true, + String? description, }) { final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' @@ -64,8 +65,11 @@ void main() { final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; return ''' name: $name +description: $description ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} @@ -327,6 +331,95 @@ ${devDependenciesSection()} ); }); + test('fails when description is too short', () async { + final Directory pluginDirectory = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test( + 'allows short descriptions for non-app-facing parts of federated plugins', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test('fails when description is too long', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + const String description = 'This description is too long. It just goes ' + 'on and on and on and on and on. pub.dev will down-score it because ' + 'there is just too much here. Someone shoul really cut this down to just ' + 'the core description so that search results are more useful and the ' + 'package does not lose pub points.'; + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: description)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + test('fails when environment section is out of order', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); From f6d93a765063befe13cab7fe6b1ac17104ff9f4e Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Fri, 1 Oct 2021 09:02:17 +1300 Subject: [PATCH 090/259] [google_maps_flutter] Add Marker drag events (#2838) --- .../google_maps_flutter/CHANGELOG.md | 4 + .../googlemaps/GoogleMapController.java | 8 +- .../plugins/googlemaps/MarkersController.java | 22 +++ .../googlemaps/MarkersControllerTest.java | 131 +++++++++++++++ .../example/lib/drag_marker.dart | 156 ++++++++++++++++++ .../google_maps_flutter/example/lib/main.dart | 2 + .../example/lib/place_marker.dart | 2 +- .../ios/Classes/GoogleMapController.m | 10 ++ .../ios/Classes/GoogleMapMarkerController.h | 2 + .../ios/Classes/GoogleMapMarkerController.m | 22 +++ .../lib/src/controller.dart | 6 + .../lib/src/google_map.dart | 26 ++- .../google_maps_flutter/pubspec.yaml | 4 +- .../test/map_creation_test.dart | 10 ++ 14 files changed, 399 insertions(+), 6 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 5406dc50e04a..0c95abda319b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.11 + +* Add additional marker drag events. + ## 2.0.10 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 056e10631011..9b8810354b8f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -467,10 +467,14 @@ public boolean onMarkerClick(Marker marker) { } @Override - public void onMarkerDragStart(Marker marker) {} + public void onMarkerDragStart(Marker marker) { + markersController.onMarkerDragStart(marker.getId(), marker.getPosition()); + } @Override - public void onMarkerDrag(Marker marker) {} + public void onMarkerDrag(Marker marker) { + markersController.onMarkerDrag(marker.getId(), marker.getPosition()); + } @Override public void onMarkerDragEnd(Marker marker) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 189cba03c1cd..47ffe9b857d6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -105,6 +105,28 @@ boolean onMarkerTap(String googleMarkerId) { return false; } + void onMarkerDragStart(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDragStart", data); + } + + void onMarkerDrag(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDrag", data); + } + void onMarkerDragEnd(String googleMarkerId, LatLng latLng) { String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); if (markerId == null) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java new file mode 100644 index 000000000000..afe91d77e2be --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.google.android.gms.internal.maps.zzt; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodCodec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.mockito.Mockito; + +public class MarkersControllerTest { + + @Test + public void controller_OnMarkerDragStart() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragStart(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragStart", data); + } + + @Test + public void controller_OnMarkerDragEnd() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragEnd(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragEnd", data); + } + + @Test + public void controller_OnMarkerDrag() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDrag(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDrag", data); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart new file mode 100644 index 000000000000..2c7929115247 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class DragMarkerPage extends GoogleMapExampleAppPage { + DragMarkerPage() : super(const Icon(Icons.drag_handle), 'Drag marker'); + + @override + Widget build(BuildContext context) { + return const DragMarkerBody(); + } +} + +class DragMarkerBody extends StatefulWidget { + const DragMarkerBody(); + + @override + State createState() => DragMarkerBodyState(); +} + +typedef MarkerUpdateAction = Marker Function(Marker marker); + +class DragMarkerBodyState extends State { + DragMarkerBodyState(); + static const LatLng center = LatLng(-33.86711, 151.1947171); + + GoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + void _onMarkerTapped(MarkerId markerId) { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + if (markers.containsKey(selectedMarker)) { + final Marker resetOld = markers[selectedMarker]! + .copyWith(iconParam: BitmapDescriptor.defaultMarker); + markers[selectedMarker!] = resetOld; + } + selectedMarker = markerId; + final Marker newMarker = tappedMarker.copyWith( + iconParam: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ); + markers[markerId] = newMarker; + }); + } + } + + void _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + this.markerPosition = newPosition; + }); + } + + void _add() { + final int markerCount = markers.length; + + if (markerCount == 12) { + return; + } + + final String markerIdVal = 'marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final Marker marker = Marker( + draggable: true, + markerId: markerId, + position: LatLng( + center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + ); + + setState(() { + markers[markerId] = marker; + }); + } + + void _remove() { + setState(() { + if (markers.containsKey(selectedMarker)) { + markers.remove(selectedMarker); + } + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Center( + child: GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: center, + zoom: 11.0, + ), + markers: markers.values.toSet(), + ), + ), + ), + Container( + height: 30, + padding: EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + markerPosition == null + ? Container() + : Expanded(child: Text("lat: ${markerPosition!.latitude}")), + markerPosition == null + ? Container() + : Expanded(child: Text("lng: ${markerPosition!.longitude}")), + ], + ), + ), + Row( + children: [ + TextButton( + child: const Text('add'), + onPressed: _add, + ), + TextButton( + child: const Text('remove'), + onPressed: _remove, + ), + ], + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 15b14db0357a..16f242c9e0ce 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -5,6 +5,7 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_example/drag_marker.dart'; import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; import 'map_click.dart'; @@ -34,6 +35,7 @@ final List _allPages = [ PlacePolylinePage(), PlacePolygonPage(), PlaceCirclePage(), + DragMarkerPage(), PaddingPage(), SnapshotPage(), LiteModePage(), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 3d083e5f9fa9..53f553eb67f8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -30,7 +30,7 @@ class PlaceMarkerBody extends StatefulWidget { State createState() => PlaceMarkerBodyState(); } -typedef Marker MarkerUpdateAction(Marker marker); +typedef MarkerUpdateAction = Marker Function(Marker marker); class PlaceMarkerBodyState extends State { PlaceMarkerBodyState(); 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 be3728753a5d..1428de69885b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -509,6 +509,16 @@ - (void)mapView:(GMSMapView*)mapView didEndDraggingMarker:(GMSMarker*)marker { [_markersController onMarkerDragEnd:markerId coordinate:marker.position]; } +- (void)mapView:(GMSMapView*)mapView didStartDraggingMarker:(GMSMarker*)marker { + NSString* markerId = marker.userData[0]; + [_markersController onMarkerDragStart:markerId coordinate:marker.position]; +} + +- (void)mapView:(GMSMapView*)mapView didDragMarker:(GMSMarker*)marker { + NSString* markerId = marker.userData[0]; + [_markersController onMarkerDrag:markerId coordinate:marker.position]; +} + - (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker { NSString* markerId = marker.userData[0]; [_markersController onInfoWindowTap:markerId]; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h index d3e835435ed9..56251872c4fb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h @@ -45,7 +45,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)changeMarkers:(NSArray*)markersToChange; - (void)removeMarkerIds:(NSArray*)markerIdsToRemove; - (BOOL)onMarkerTap:(NSString*)markerId; +- (void)onMarkerDragStart:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; - (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; +- (void)onMarkerDrag:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; - (void)onInfoWindowTap:(NSString*)markerId; - (void)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result; - (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index 6a9fb885afac..51ed825fa7d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -295,6 +295,28 @@ - (BOOL)onMarkerTap:(NSString*)markerId { [_methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : markerId}]; return controller.consumeTapEvents; } +- (void)onMarkerDragStart:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { + if (!markerId) { + return; + } + FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"marker#onDragStart" + arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; +} +- (void)onMarkerDrag:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { + if (!markerId) { + return; + } + FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"marker#onDrag" + arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; +} - (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { if (!markerId) { return; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index ba18c5ffc17b..d57ac97b1663 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -69,6 +69,12 @@ class GoogleMapController { GoogleMapsFlutterPlatform.instance .onMarkerTap(mapId: mapId) .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); + GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position)); GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( (MarkerDragEndEvent e) => _googleMapState.onMarkerDragEnd(e.value, e.position)); diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 26b9d6b83c84..b4ffd22360e8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -8,7 +8,7 @@ part of google_maps_flutter; /// /// Pass to [GoogleMap.onMapCreated] to receive a [GoogleMapController] when the /// map is created. -typedef void MapCreatedCallback(GoogleMapController controller); +typedef MapCreatedCallback = void Function(GoogleMapController controller); // This counter is used to provide a stable "constant" initialization id // to the buildView function, so the web implementation can use it as a @@ -368,6 +368,30 @@ class _GoogleMapState extends State { } } + void onMarkerDragStart(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDragStart'); + } + final ValueChanged? onDragStart = marker.onDragStart; + if (onDragStart != null) { + onDragStart(position); + } + } + + void onMarkerDrag(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDrag'); + } + final ValueChanged? onDrag = marker.onDrag; + if (onDrag != null) { + onDrag(position); + } + } + void onMarkerDragEnd(MarkerId markerId, LatLng position) { assert(markerId != null); final Marker? marker = _markers[markerId]; diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 641e475a56f0..61ac88a8f10d 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/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.10 +version: 2.0.11 environment: sdk: ">=2.14.0 <3.0.0" @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.0.0 + google_maps_flutter_platform_interface: ^2.1.2 dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 6e0f5ed3e4f5..6b3ac906802f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -229,6 +229,16 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + @override Stream onMarkerDragEnd({required int mapId}) { return mapEventStreamController.stream.whereType(); From d9490e76efdd71bf7f2d88fe85e66bd4ab5426f8 Mon Sep 17 00:00:00 2001 From: Murray-Meller Date: Sat, 2 Oct 2021 02:48:05 +1000 Subject: [PATCH 091/259] [google_maps_flutter]: LatLng longitude loses precision in constructor #90574 (#4374) --- .../CHANGELOG.md | 5 ++ .../lib/src/types/location.dart | 7 ++- .../pubspec.yaml | 2 +- .../test/types/location_test.dart | 62 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart 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 464c33ed754e..3a22dde8b659 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.3 + +* `LatLng` constructor maintains longitude precision when given within + acceptable range + ## 2.1.2 * Add additional marker drag events 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 42c66e036fd7..7a1aaf051388 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -14,13 +14,16 @@ class LatLng { /// The latitude is clamped to the inclusive interval from -90.0 to +90.0. /// /// The longitude is normalized to the half-open interval from -180.0 - /// (inclusive) to +180.0 (exclusive) + /// (inclusive) to +180.0 (exclusive). const LatLng(double latitude, double longitude) : assert(latitude != null), assert(longitude != null), latitude = (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), - longitude = (longitude + 180.0) % 360.0 - 180.0; + // Avoids normalization if possible to prevent unnecessary loss of precision + longitude = longitude >= -180 && longitude < 180 + ? longitude + : (longitude + 180.0) % 360.0 - 180.0; /// The latitude in degrees between -90.0 and 90.0, both inclusive. final double latitude; 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 2a2c9cfa8b46..d3d7653b746e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ 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.2 +version: 2.1.3 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart new file mode 100644 index 000000000000..80f696177dfd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LanLng constructor', () { + test('Maintains longitude precision if within acceptable range', () async { + const lat = -34.509981; + const lng = 150.792384; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(lng)); + }); + + test('Normalizes longitude that is below lower limit', () async { + const lat = -34.509981; + const lng = -270.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(90.0)); + }); + + test('Normalizes longitude that is above upper limit', () async { + const lat = -34.509981; + const lng = 270.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-90.0)); + }); + + test('Includes longitude set to lower limit', () async { + const lat = -34.509981; + const lng = -180.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-180.0)); + }); + + test('Normalizes longitude set to upper limit', () async { + const lat = -34.509981; + const lng = 180.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-180.0)); + }); + }); +} From a12791cbeb4e0a855c473ed29c448fb4e9fd50f3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 1 Oct 2021 16:28:06 -0400 Subject: [PATCH 092/259] Fix order-dependant platform interface tests (#4406) --- .../test/file_selector_platform_interface_test.dart | 6 ++++-- .../google_maps_flutter_platform_test.dart | 7 +++++-- .../test/google_sign_in_platform_interface_test.dart | 5 ++++- .../test/quick_actions_platform_interface_test.dart | 5 ++++- .../test/method_channel_url_launcher_test.dart | 6 ++++-- .../test/method_channel_video_player_test.dart | 6 ++++-- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index a5b22b977d69..91e78b452961 100644 --- a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart @@ -7,10 +7,12 @@ import 'package:file_selector_platform_interface/src/method_channel/method_chann import 'package:flutter_test/flutter_test.dart'; void main() { + // Store the initial instance before any tests change it. + final FileSelectorPlatform initialInstance = FileSelectorPlatform.instance; + group('$FileSelectorPlatform', () { test('$MethodChannelFileSelector() is the default instance', () { - expect(FileSelectorPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Can be extended', () { 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 de4edf375696..c381f9e30750 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -16,10 +16,13 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final GoogleMapsFlutterPlatform initialInstance = + GoogleMapsFlutterPlatform.instance; + group('$GoogleMapsFlutterPlatform', () { test('$MethodChannelGoogleMapsFlutter() is the default instance', () { - expect(GoogleMapsFlutterPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart index b3ac51b7fa52..a3450b6f3fb9 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -7,9 +7,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; void main() { + // Store the initial instance before any tests change it. + final GoogleSignInPlatform initialInstance = GoogleSignInPlatform.instance; + group('$GoogleSignInPlatform', () { test('$MethodChannelGoogleSignIn is the default instance', () { - expect(GoogleSignInPlatform.instance, isA()); + expect(initialInstance, isA()); }); test('Cannot be implemented with `implements`', () { diff --git a/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart index 8ce40816217f..d1c8798aa65f 100644 --- a/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart +++ b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart @@ -9,9 +9,12 @@ import 'package:quick_actions_platform_interface/platform_interface/quick_action void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final QuickActionsPlatform initialInstance = QuickActionsPlatform.instance; + group('$QuickActionsPlatform', () { test('$MethodChannelQuickActions is the default instance', () { - expect(QuickActionsPlatform.instance, isA()); + expect(initialInstance, isA()); }); test('Cannot be implemented with `implements`', () { diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart index 5bfc78c5c5a2..23d9a4534f7b 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart @@ -14,10 +14,12 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final UrlLauncherPlatform initialInstance = UrlLauncherPlatform.instance; + group('$UrlLauncherPlatform', () { test('$MethodChannelUrlLauncher() is the default instance', () { - expect(UrlLauncherPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { 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 9da71617e66a..f5439b844045 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -92,10 +92,12 @@ class _ApiLogger implements TestHostVideoPlayerApi { void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final VideoPlayerPlatform initialInstance = VideoPlayerPlatform.instance; + group('$VideoPlayerPlatform', () { test('$MethodChannelVideoPlayer() is the default instance', () { - expect(VideoPlayerPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); }); From dc7b8641174fc34a3c895e6fc17d88fbdd322cdd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sat, 2 Oct 2021 09:48:04 -0400 Subject: [PATCH 093/259] [ci] Remove obsolete Dockerfile (#4405) --- .ci/java8.Dockerfile | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .ci/java8.Dockerfile diff --git a/.ci/java8.Dockerfile b/.ci/java8.Dockerfile deleted file mode 100644 index fb1844a5401f..000000000000 --- a/.ci/java8.Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM cirrusci/flutter:stable - -RUN apt-get update -y - -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk -ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 - -RUN apt-get install -y --no-install-recommends gnupg - -# Add repo for gcloud sdk and install it -RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ - tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - -RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ - apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - - -RUN apt-get update && apt-get install -y google-cloud-sdk && \ - gcloud config set core/disable_usage_reporting true && \ - gcloud config set component_manager/disable_update_check true - -RUN yes | sdkmanager \ - "platforms;android-27" \ - "build-tools;27.0.3" \ - "extras;google;m2repository" \ - "extras;android;m2repository" - -RUN yes | sdkmanager --licenses - -# Install formatter. -RUN apt-get install -y clang-format -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk From 0558169b893075b13dcea5dbb5b2dec2f714ee6f Mon Sep 17 00:00:00 2001 From: Nick Bradshaw Date: Tue, 5 Oct 2021 07:24:04 -0700 Subject: [PATCH 094/259] [webview_flutter] Add zoomEnabled to webview flutter platform interface (#4404) This is the first-step task to add a zoomEnabled param on the WebView widget. --- .../webview_flutter_platform_interface/CHANGELOG.md | 4 ++++ .../lib/src/method_channel/webview_method_channel.dart | 1 + .../lib/src/types/web_settings.dart | 4 ++++ .../webview_flutter_platform_interface/pubspec.yaml | 2 +- .../test/src/method_channel/webview_method_channel_test.dart | 3 ++- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 9e217a04e961..93c470cee069 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +* Add `zoomEnabled` functionality to `WebSettings`. + ## 1.0.0 * Extracted platform interface from `webview_flutter`. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/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 b467daf72a08..411cad038618 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 @@ -200,6 +200,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { _addIfNonNull( 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); _addSettingIfPresent('userAgent', settings.userAgent); + _addIfNonNull('zoomEnabled', settings.zoomEnabled); return map; } 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 48b2de9c1ca0..3d94153c886e 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 @@ -78,6 +78,7 @@ class WebSettings { this.debuggingEnabled, this.gestureNavigationEnabled, this.allowsInlineMediaPlayback, + this.zoomEnabled, required this.userAgent, }) : assert(userAgent != null); @@ -111,6 +112,9 @@ class WebSettings { /// See also [WebView.userAgent]. final WebSetting userAgent; + /// Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. + final bool? zoomEnabled; + /// Whether to allow swipe based navigation in iOS. /// /// See also: [WebView.gestureNavigationEnabled] diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index bf43c265d77a..166c55b133db 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/webview_flut 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.0.0 +version: 1.1.0 environment: sdk: ">=2.12.0 <3.0.0" 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 2f845eaa4999..3914b2080259 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 @@ -5,7 +5,6 @@ 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'; @@ -229,6 +228,7 @@ void main() { debuggingEnabled: true, gestureNavigationEnabled: true, allowsInlineMediaPlayback: true, + zoomEnabled: false, ); await webViewPlatform.updateSettings(settings); @@ -245,6 +245,7 @@ void main() { 'debuggingEnabled': true, 'gestureNavigationEnabled': true, 'allowsInlineMediaPlayback': true, + 'zoomEnabled': false, }, ), ], From cd5dc3ba4c543b0fb79d0d44f3414d097ebbd9e6 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 6 Oct 2021 10:28:02 +0200 Subject: [PATCH 095/259] [webview_flutter] Update webview platform interface with new methods for running JavaScript. (#4401) --- .../CHANGELOG.md | 4 ++ .../webview_method_channel.dart | 16 +++++++- .../webview_platform_controller.dart | 24 +++++++++-- .../lib/src/types/javascript_channel.dart | 6 +-- .../pubspec.yaml | 2 +- .../webview_method_channel_test.dart | 41 +++++++++++++++++-- 6 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 93c470cee069..04641f97dc79 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +* Added `runJavascript` and `runJavascriptReturningResult` interface methods to supersede `evaluateJavascript`. + ## 1.1.0 * Add `zoomEnabled` functionality to `WebSettings`. 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 411cad038618..9610038eec82 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 @@ -123,9 +123,21 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future evaluateJavascript(String javascriptString) { + Future evaluateJavascript(String javascript) { return _channel - .invokeMethod('evaluateJavascript', javascriptString) + .invokeMethod('evaluateJavascript', javascript) + .then((result) => result!); + } + + @override + Future runJavascript(String javascript) async { + await _channel.invokeMethod('runJavascript', javascript); + } + + @override + Future runJavascriptReturningResult(String javascript) { + return _channel + .invokeMethod('runJavascriptReturningResult', javascript) .then((result) => result!); } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart index 319ca7e7a845..b42da4326079 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -106,12 +106,30 @@ abstract class WebViewPlatformController { /// Evaluates a JavaScript expression in the context of the current page. /// /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { + /// evaluated expression is not supported (e.g on iOS not all non-primitive types can be evaluated). + Future evaluateJavascript(String javascript) { throw UnimplementedError( "WebView evaluateJavascript is not implemented on the current platform"); } + /// Runs the given JavaScript in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred. + Future runJavascript(String javascript) { + throw UnimplementedError( + "WebView runJavascript is not implemented on the current platform"); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the + /// type the given expression evaluates to is unsupported. Unsupported values include + /// certain non-primitive types on iOS, as well as `undefined` or `null` on iOS 14+. + Future runJavascriptReturningResult(String javascript) { + throw UnimplementedError( + "WebView runJavascriptReturningResult is not implemented on the current platform"); + } + /// Adds new JavaScript channels to the set of enabled channels. /// /// For each value in this list the platform's webview should make sure that a corresponding @@ -130,7 +148,7 @@ abstract class WebViewPlatformController { /// Removes JavaScript channel names from the set of enabled channels. /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through + /// This disables channels that were previously enabled by [addJavascriptChannels] or through /// [CreationParams.javascriptChannelNames]. Future removeJavascriptChannels(Set javascriptChannelNames) { throw UnimplementedError( diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart index 8b31f5b6061e..f32a41893eb5 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart @@ -4,14 +4,14 @@ import 'javascript_message.dart'; -/// Callback type for handling messages sent from Javascript running in a web view. +/// Callback type for handling messages sent from JavaScript running in a web view. typedef void JavascriptMessageHandler(JavascriptMessage message); final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); /// A named channel for receiving messaged from JavaScript code running inside a web view. class JavascriptChannel { - /// Constructs a Javascript channel. + /// Constructs a JavaScript channel. /// /// The parameters `name` and `onMessageReceived` must not be null. JavascriptChannel({ @@ -24,7 +24,7 @@ class JavascriptChannel { /// The channel's name. /// /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. + /// the JavaScript window object's property named `name`. /// /// The name must start with a letter or underscore(_), followed by any combination of those /// characters plus digits. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 166c55b133db..994c3dcdebdf 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/webview_flut 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.1.0 +version: 1.2.0 environment: sdk: ">=2.12.0 <3.0.0" 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 3914b2080259..85f184f9f715 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 @@ -30,6 +30,7 @@ void main() { case 'canGoBack': case 'canGoForward': return true; + case 'runJavascriptReturningResult': case 'evaluateJavascript': return methodCall.arguments as String; case 'getScrollX': @@ -266,16 +267,50 @@ void main() { test('evaluateJavascript', () async { final String evaluateJavascript = await webViewPlatform.evaluateJavascript( - 'This simulates some Javascript code.', + 'This simulates some JavaScript code.', ); - expect('This simulates some Javascript code.', evaluateJavascript); + expect('This simulates some JavaScript code.', evaluateJavascript); expect( log, [ isMethodCall( 'evaluateJavascript', - arguments: 'This simulates some Javascript code.', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascript', () async { + await webViewPlatform.runJavascript( + 'This simulates some JavaScript code.', + ); + + expect( + log, + [ + isMethodCall( + 'runJavascript', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascriptReturningResult', () async { + final String evaluateJavascript = + await webViewPlatform.runJavascriptReturningResult( + 'This simulates some JavaScript code.', + ); + + expect('This simulates some JavaScript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'runJavascriptReturningResult', + arguments: 'This simulates some JavaScript code.', ), ], ); From 174f140651e9ca9c958f2ae75960684c27772a07 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Wed, 6 Oct 2021 17:03:05 -0700 Subject: [PATCH 096/259] [camera] Add filter for unsupported cameras on Android (#4418) --- .../flutter/plugins/camera/CameraUtils.java | 10 +++++ .../plugins/camera/CameraUtilsTest.java | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 003d80a6c241..11b6eeaa5b50 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -97,6 +97,16 @@ public static List> getAvailableCameras(Activity activity) String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); for (String cameraName : cameraNames) { + int cameraId; + try { + cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + cameraId = -1; + } + if (cameraId < 0) { + continue; + } + HashMap details = new HashMap<>(); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); details.put("name", cameraName); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index 6b714ce41e34..e59b05bf4fe3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -5,8 +5,20 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.List; +import java.util.Map; import org.junit.Test; public class CameraUtilsTest { @@ -52,4 +64,37 @@ public void deserializeDeviceOrientation_deserializesCorrectly() { public void deserializeDeviceOrientation_throwsForNull() { CameraUtils.deserializeDeviceOrientation(null); } + + @Test + public void getAvailableCameras_retrievesValidCameras() + throws CameraAccessException, NumberFormatException { + final Activity mockActivity = mock(Activity.class); + final CameraManager mockCameraManager = mock(CameraManager.class); + final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); + final String[] mockCameraIds = {"1394902", "-192930", "0283835", "foobar"}; + final int mockSensorOrientation0 = 90; + final int mockSensorOrientation2 = 270; + final int mockLensFacing0 = CameraMetadata.LENS_FACING_FRONT; + final int mockLensFacing2 = CameraMetadata.LENS_FACING_EXTERNAL; + + when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); + when(mockCameraManager.getCameraIdList()).thenReturn(mockCameraIds); + when(mockCameraManager.getCameraCharacteristics(anyString())) + .thenReturn(mockCameraCharacteristics); + when(mockCameraCharacteristics.get(any())) + .thenReturn(mockSensorOrientation0) + .thenReturn(mockLensFacing0) + .thenReturn(mockSensorOrientation2) + .thenReturn(mockLensFacing2); + + List> availableCameras = CameraUtils.getAvailableCameras(mockActivity); + + assertEquals(availableCameras.size(), 2); + assertEquals(availableCameras.get(0).get("name"), "1394902"); + assertEquals(availableCameras.get(0).get("sensorOrientation"), mockSensorOrientation0); + assertEquals(availableCameras.get(0).get("lensFacing"), "front"); + assertEquals(availableCameras.get(1).get("name"), "0283835"); + assertEquals(availableCameras.get(1).get("sensorOrientation"), mockSensorOrientation2); + assertEquals(availableCameras.get(1).get("lensFacing"), "external"); + } } From 5117a3fcd1064ec0b66e4846fcad7f01c9149b73 Mon Sep 17 00:00:00 2001 From: Pavel Shapovalov Date: Fri, 8 Oct 2021 22:38:03 +0300 Subject: [PATCH 097/259] [google_sign_in] Add serverAuthCode attribute to google_sign_in_platform_interface user data (#4179) --- .../google_sign_in_platform_interface/CHANGELOG.md | 4 ++++ .../lib/src/types.dart | 11 ++++++++--- .../lib/src/utils.dart | 3 ++- .../google_sign_in_platform_interface/pubspec.yaml | 2 +- .../test/method_channel_google_sign_in_test.dart | 2 ++ 5 files changed, 17 insertions(+), 5 deletions(-) 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 ee43db685339..917864173f7d 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Add serverAuthCode attribute to user data + ## 2.0.1 * Updates `init` function in `MethodChannelGoogleSignIn` to parametrize `clientId` property. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index 61231d1b70b9..e30966f87598 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -31,6 +31,7 @@ class GoogleSignInUserData { this.displayName, this.photoUrl, this.idToken, + this.serverAuthCode, }); /// The display name of the signed in user. @@ -66,9 +67,12 @@ class GoogleSignInUserData { /// data. String? idToken; + /// Server auth code used to access Google Login + String? serverAuthCode; + @override - int get hashCode => - hashObjects([displayName, email, id, photoUrl, idToken]); + int get hashCode => hashObjects( + [displayName, email, id, photoUrl, idToken, serverAuthCode]); @override bool operator ==(dynamic other) { @@ -79,7 +83,8 @@ class GoogleSignInUserData { otherUserData.email == email && otherUserData.id == id && otherUserData.photoUrl == photoUrl && - otherUserData.idToken == idToken; + otherUserData.idToken == idToken && + otherUserData.serverAuthCode == serverAuthCode; } } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 4a70ec4d25ef..0d89835fb498 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart @@ -14,7 +14,8 @@ GoogleSignInUserData? getUserDataFromMap(Map? data) { id: data['id']!, displayName: data['displayName'], photoUrl: data['photoUrl'], - idToken: data['idToken']); + idToken: data['idToken'], + serverAuthCode: data['serverAuthCode']); } /// Converts token data coming from native code into the proper platform interface type. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 75b3d98b562d..aa41039cdf5a 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 +version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" 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 390c12583a79..fcb443f84293 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart @@ -13,6 +13,8 @@ const Map kUserData = { "id": "8162538176523816253123", "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", "displayName": "John Doe", + 'idToken': '123', + 'serverAuthCode': '789', }; const Map kTokenData = { From c254963c1e2b2badb1910d53d6f9bbe8da9d1f5a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 8 Oct 2021 19:03:06 -0400 Subject: [PATCH 098/259] [webview_flutter] Adjust test URLs again (#4407) --- .../webview_flutter_test.dart | 51 ++++++++++--------- .../webview_flutter_test.dart | 51 ++++++++++--------- .../webview_flutter_test.dart | 51 ++++++++++--------- 3 files changed, 81 insertions(+), 72 deletions(-) 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 896366798b18..3379bafa2346 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 @@ -18,6 +18,13 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + const bool _skipDueToIssue86757 = true; // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -29,7 +36,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -38,7 +45,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -50,7 +57,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -58,9 +65,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.example.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -74,7 +81,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -292,7 +299,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1099,12 +1106,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com/"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1271,12 +1277,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1292,7 +1297,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1303,7 +1308,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', @@ -1327,11 +1332,10 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, // Flaky on Android: https://github.com/flutter/flutter/issues/86757 skip: Platform.isAndroid && _skipDueToIssue86757); @@ -1355,25 +1359,24 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.example.com/")'); + await controller.evaluateJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.example.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, skip: _skipDueToIssue86757, ); 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 3dab048c8ff2..c57d2bd55580 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 @@ -22,6 +22,13 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + const bool _skipDueToIssue86757 = true; // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -34,7 +41,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -44,7 +51,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -56,7 +63,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -64,9 +71,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.example.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -80,7 +87,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -298,7 +305,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1053,12 +1060,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com/"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1220,12 +1226,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1241,7 +1246,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1252,7 +1257,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', @@ -1276,11 +1281,10 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, // Flaky on Android: https://github.com/flutter/flutter/issues/86757 skip: _skipDueToIssue86757); @@ -1304,25 +1308,24 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.example.com/")'); + await controller.evaluateJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.example.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, skip: _skipDueToIssue86757, ); 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 c080c46120ec..8ba17a2428c5 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 @@ -20,6 +20,13 @@ import 'package:webview_flutter_wkwebview_example/web_view.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. const bool _skipDueToIssue86757 = false; @@ -32,7 +39,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -41,7 +48,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -53,7 +60,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -61,9 +68,9 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.example.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); testWidgets('loadUrl with headers', (WidgetTester tester) async { @@ -76,7 +83,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -292,7 +299,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -912,12 +919,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com/"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1084,12 +1090,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.example.com"'); + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.example.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1105,7 +1110,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1116,7 +1121,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', @@ -1140,11 +1145,10 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. @@ -1166,25 +1170,24 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.example.com/")'); + await controller.evaluateJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.example.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, skip: _skipDueToIssue86757, ); From 6716d75c08cb04b6bf7ca1ffc5a9592618f87537 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 13 Oct 2021 10:19:26 -0700 Subject: [PATCH 099/259] Update integration_test README (#3824) * Change deprecated to moved - add a link to the flutter.dev docs - add a link to the new location - Changed "deprecated" to "moved" to avoid confusion, since this is still the recommended way to write unit tests, but needs to be imported differently. * Update packages/integration_test/README.md * Apply suggestions from code review Co-authored-by: Shams Zakhour (ignore Sfshaza) <44418985+sfshaza2@users.noreply.github.com> Co-authored-by: Shams Zakhour (ignore Sfshaza) <44418985+sfshaza2@users.noreply.github.com> --- packages/integration_test/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 67f658b56327..6bf388131680 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -1,9 +1,12 @@ -# integration_test (deprecated) +# integration_test (moved) -## DEPRECATED +## MOVED -This package has been moved to the Flutter SDK. Starting with Flutter 2.0, -it should be included as: +This package has [moved to the Flutter +SDK](https://github.com/flutter/flutter/tree/master/packages/integration_test), +and the pub.dev version is deprecated. +As of Flutter 2.0, include it in your pubspec's +dev dependencies section, as follows: ``` dev_dependencies: @@ -11,6 +14,9 @@ dev_dependencies: sdk: flutter ``` +For the latest documentation, see [Integration +testing](https://flutter.dev/docs/testing/integration-tests). + ## Old instructions This package enables self-driving testing of Flutter code on devices and emulators. From 176cfb8083bc85eb071599ee8f5ef65b732b8ffd Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 02:21:56 +0200 Subject: [PATCH 100/259] [camera] Update [CameraOrientationTests testOrientationNotifications] unit test to work on Xcode 13 (#4426) --- packages/camera/camera/CHANGELOG.md | 5 ++- .../ios/RunnerTests/CameraOrientationTests.m | 40 +++++++++++-------- .../camera/camera/ios/Classes/CameraPlugin.m | 2 + .../camera/ios/Classes/CameraPlugin.modulemap | 10 +++++ .../camera/ios/Classes/CameraPlugin_Test.h | 19 +++++++++ .../camera/ios/Classes/camera-umbrella.h | 9 +++++ packages/camera/camera/ios/camera.podspec | 3 +- packages/camera/camera/pubspec.yaml | 2 +- 8 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 packages/camera/camera/ios/Classes/CameraPlugin.modulemap create mode 100644 packages/camera/camera/ios/Classes/CameraPlugin_Test.h create mode 100644 packages/camera/camera/ios/Classes/camera-umbrella.h diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index c9dfc63eb46c..dc83a4fabca2 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.9.4+2 -* Updated package description. +* Updated package description; +* Refactor unit test on iOS to make it compatible with new restrictions in Xcode 13 which only supports the use of the `XCUIDevice` in Xcode UI tests. ## 0.9.4+1 diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index 6c29ef7b2866..246eab90a919 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -3,30 +3,29 @@ // found in the LICENSE file. @import camera; +@import camera.Test; @import XCTest; +@import Flutter; #import @interface CameraOrientationTests : XCTestCase -@property(strong, nonatomic) id mockRegistrar; @property(strong, nonatomic) id mockMessenger; +@property(strong, nonatomic) CameraPlugin *cameraPlugin; @end @implementation CameraOrientationTests - (void)setUp { [super setUp]; - self.mockRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - OCMStub([self.mockRegistrar messenger]).andReturn(self.mockMessenger); + self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger]; } - (void)testOrientationNotifications { id mockMessenger = self.mockMessenger; [mockMessenger setExpectationOrderMatters:YES]; - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationPortrait; - - [CameraPlugin registerWithRegistrar:self.mockRegistrar]; [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown"]; [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp"]; @@ -34,34 +33,43 @@ - (void)testOrientationNotifications { [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight"]; OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); - // No notification when orientation doesn't change. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationLandscapeLeft; + // No notification when flat. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationFaceUp; + [self.cameraPlugin + orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceUp]]; // No notification when facedown. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationFaceDown; + [self.cameraPlugin + orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceDown]]; OCMVerifyAll(mockMessenger); } - (void)rotate:(UIDeviceOrientation)deviceOrientation - expectedChannelOrientation:(NSString*)channelOrientation { + expectedChannelOrientation:(NSString *)channelOrientation { id mockMessenger = self.mockMessenger; - XCTestExpectation* orientationExpectation = [self expectationWithDescription:channelOrientation]; + XCTestExpectation *orientationExpectation = [self expectationWithDescription:channelOrientation]; OCMExpect([mockMessenger sendOnChannel:[OCMArg any] - message:[OCMArg checkWithBlock:^BOOL(NSData* data) { - NSObject* codec = [FlutterStandardMethodCodec sharedInstance]; - FlutterMethodCall* methodCall = [codec decodeMethodCall:data]; + message:[OCMArg checkWithBlock:^BOOL(NSData *data) { + NSObject *codec = [FlutterStandardMethodCodec sharedInstance]; + FlutterMethodCall *methodCall = [codec decodeMethodCall:data]; [orientationExpectation fulfill]; return [methodCall.method isEqualToString:@"orientation_changed"] && [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; }]]); - XCUIDevice.sharedDevice.orientation = deviceOrientation; + [self.cameraPlugin + orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation { + UIDevice *mockDevice = OCMClassMock([UIDevice class]); + OCMStub([mockDevice orientation]).andReturn(deviceOrientation); + + return [NSNotification notificationWithName:@"orientation_test" object:mockDevice]; +} + @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index da560d6c4df7..78c5a34ab05a 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -3,6 +3,8 @@ // found in the LICENSE file. #import "CameraPlugin.h" +#import "CameraPlugin_Test.h" + #import #import #import diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap new file mode 100644 index 000000000000..30afa91bdda2 --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap @@ -0,0 +1,10 @@ +framework module camera { + umbrella header "camera-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "CameraPlugin_Test.h" + } +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h new file mode 100644 index 000000000000..6952ae0a4aaa --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.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. + +// This header is available in the Test module. Import via "@import camera.Test;" + +#import + +/// Methods exposed for unit testing. +@interface CameraPlugin () + +- (instancetype)initWithRegistry:(NSObject *)registry + messenger:(NSObject *)messenger + NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +- (void)orientationChanged:(NSNotification *)notification; + +@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h new file mode 100644 index 000000000000..5c39401e6261 --- /dev/null +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +FOUNDATION_EXPORT double cameraVersionNumber; +FOUNDATION_EXPORT const unsigned char cameraVersionString[]; diff --git a/packages/camera/camera/ios/camera.podspec b/packages/camera/camera/ios/camera.podspec index 4a142bd4589a..5906bf98af45 100644 --- a/packages/camera/camera/ios/camera.podspec +++ b/packages/camera/camera/ios/camera.podspec @@ -13,8 +13,9 @@ A Flutter plugin to use the camera from your Flutter app. s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/camera' } s.documentation_url = 'https://pub.dev/packages/camera' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/CameraPlugin.modulemap' s.dependency 'Flutter' s.platform = :ios, '9.0' diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 21892b213781..f68d026b206c 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/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+1 +version: 0.9.4+2 environment: sdk: ">=2.14.0 <3.0.0" From 9e46048ad2e1f085c1e8f6c77391fa52025e681f Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 15 Oct 2021 18:18:02 -0700 Subject: [PATCH 101/259] Bump compileSdkVersion to 31 (#4432) --- packages/camera/camera/android/build.gradle | 2 +- script/tool/lib/src/create_all_plugins_app_command.dart | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 633efd0b284a..25285ad33205 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -1,6 +1,6 @@ group 'io.flutter.plugins.camera' version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] +def args = ["-Xlint:deprecation","-Xlint:unchecked"] buildscript { repositories { 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 6dbebf2f5c74..5d9b4ed9c728 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -93,10 +93,13 @@ class CreateAllPluginsAppCommand extends PluginCommand { final StringBuffer newGradle = StringBuffer(); for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion 16')) { - // Android SDK 20 is required by Google maps. - // Android SDK 19 is required by WebView. + if (line.contains('minSdkVersion')) { + // minSdkVersion 20 is required by Google maps. + // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); + } else if (line.contains('compileSdkVersion')) { + // compileSdkVersion 31 is required by Camera. + newGradle.writeln('compileSdkVersion 31'); } else { newGradle.writeln(line); } From aae841aa5a7062f05e0aba4f9304dd605dbbc2b2 Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Mon, 18 Oct 2021 23:38:03 +0530 Subject: [PATCH 102/259] [image_picker_for_web] Added support for maxWidth, maxHeight and imageQuality (#4389) --- .../image_picker_for_web/CHANGELOG.md | 5 + .../image_picker_for_web/README.md | 5 +- .../integration_test/image_resizer_test.dart | 128 ++++++++++++++++++ .../lib/image_picker_for_web.dart | 51 +++++-- .../lib/src/image_resizer.dart | 83 ++++++++++++ .../lib/src/image_resizer_utils.dart | 33 +++++ .../image_picker_for_web/pubspec.yaml | 2 +- .../test/image_resizer_utils_test.dart | 92 +++++++++++++ 8 files changed, 382 insertions(+), 17 deletions(-) create mode 100644 packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart create mode 100644 packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart create mode 100644 packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart create mode 100644 packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index d11ead3bb64e..aee8b0863c13 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.4 + +* Implemented `maxWidth`, `maxHeight` and `imageQuality` when selecting images + (except for gifs). + ## 2.1.3 * Add `implements` to pubspec. diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 73f2dfc4b84f..c8b85f21cc89 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -43,7 +43,8 @@ Each browser may implement `capture` any way they please, so it may (or may not) difference in your users' experience. ### pickImage() -The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web. +The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images. +The argument `imageQuality` only works for jpeg and webp images. ### pickVideo() The argument `maxDuration` is not supported on the web. @@ -63,7 +64,7 @@ You should be able to use `package:image_picker` _almost_ as normal. Once the user has picked a file, the returned `PickedFile` instance will contain a `network`-accessible URL (pointing to a location within the browser). -The instace will also let you retrieve the bytes of the selected file across all platforms. +The instance will also let you retrieve the bytes of the selected file across all platforms. If you want to use the path directly, your code would need look like this: diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart new file mode 100644 index 000000000000..067c7750eb11 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_for_web/src/image_resizer.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +//This is a sample 10x10 png image +final String pngFileBase64Contents = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC"; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Under test... + late ImageResizer imageResizer; + late XFile pngFile; + setUp(() { + imageResizer = ImageResizer(); + final pngHtmlFile = _base64ToFile(pngFileBase64Contents, "pngImage.png"); + pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile), + name: pngHtmlFile.name, mimeType: pngHtmlFile.type); + }); + + testWidgets("image is loaded correctly ", (WidgetTester tester) async { + final imageElement = await imageResizer.loadImage(pngFile.path); + expect(imageElement.width!, 10); + expect(imageElement.height!, 10); + }); + + testWidgets( + "canvas is loaded with image's width and height when max width and max height are null", + (widgetTester) async { + final imageElement = await imageResizer.loadImage(pngFile.path); + final canvas = imageResizer.resizeImageElement(imageElement, null, null); + expect(canvas.width, imageElement.width); + expect(canvas.height, imageElement.height); + }); + + testWidgets( + "canvas size is scaled when max width and max height are not null", + (widgetTester) async { + final imageElement = await imageResizer.loadImage(pngFile.path); + final canvas = imageResizer.resizeImageElement(imageElement, 8, 8); + expect(canvas.width, 8); + expect(canvas.height, 8); + }); + + testWidgets("resized image is returned after converting canvas to file", + (widgetTester) async { + final imageElement = await imageResizer.loadImage(pngFile.path); + final canvas = imageResizer.resizeImageElement(imageElement, null, null); + final resizedImage = + await imageResizer.writeCanvasToFile(pngFile, canvas, null); + expect(resizedImage.name, "scaled_${pngFile.name}"); + }); + + testWidgets("image is scaled when maxWidth is set", + (WidgetTester tester) async { + final scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, 5, null, null); + expect(scaledImage.name, "scaled_${pngFile.name}"); + final scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, Size(5, 5)); + }); + + testWidgets("image is scaled when maxHeight is set", + (WidgetTester tester) async { + final scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, 6, null); + expect(scaledImage.name, "scaled_${pngFile.name}"); + final scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, Size(6, 6)); + }); + + testWidgets("image is scaled when imageQuality is set", + (WidgetTester tester) async { + final scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, null, 89); + expect(scaledImage.name, "scaled_${pngFile.name}"); + }); + + testWidgets("image is scaled when maxWidth,maxHeight,imageQuality are set", + (WidgetTester tester) async { + final scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, 3, 4, 89); + expect(scaledImage.name, "scaled_${pngFile.name}"); + }); + + testWidgets("image is not scaled when maxWidth,maxHeight, is set", + (WidgetTester tester) async { + final scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, null, null); + expect(scaledImage.name, pngFile.name); + }); +} + +Future _getImageSize(XFile file) async { + final completer = Completer(); + final image = html.ImageElement(src: file.path); + image.onLoad.listen((event) { + completer.complete(Size(image.width!.toDouble(), image.height!.toDouble())); + }); + image.onError.listen((event) { + completer.complete(Size(0, 0)); + }); + return completer.future; +} + +html.File _base64ToFile(String data, String fileName) { + var arr = data.split(','); + var bstr = html.window.atob(arr[1]); + var n = bstr.length, u8arr = Uint8List(n); + + while (n >= 1) { + u8arr[n - 1] = bstr.codeUnitAt(n - 1); + n--; + } + + return html.File([u8arr], fileName); +} diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index b170ee3256ab..f13aeb1f0a4e 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:html' as html; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:image_picker_for_web/src/image_resizer.dart'; import 'package:meta/meta.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; @@ -23,10 +24,14 @@ class ImagePickerPlugin extends ImagePickerPlatform { late html.Element _target; + late ImageResizer _imageResizer; + /// A constructor that allows tests to override the function that creates file inputs. ImagePickerPlugin({ @visibleForTesting ImagePickerPluginTestOverrides? overrides, + @visibleForTesting ImageResizer? imageResizer, }) : _overrides = overrides { + _imageResizer = imageResizer ?? ImageResizer(); _target = _ensureInitialized(_kImagePickerInputsDomId); } @@ -122,7 +127,12 @@ class ImagePickerPlugin extends ImagePickerPlatform { accept: _kAcceptImageMimeType, capture: capture, ); - return files.first; + return _imageResizer.resizeImageIfNeeded( + files.first, + maxWidth, + maxHeight, + imageQuality, + ); } /// Returns an [XFile] containing the video that was picked. @@ -157,8 +167,21 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - }) { - return getFiles(accept: _kAcceptImageMimeType, multiple: true); + }) async { + final List images = await getFiles( + accept: _kAcceptImageMimeType, + multiple: true, + ); + final Iterable> resized = images.map( + (image) => _imageResizer.resizeImageIfNeeded( + image, + maxWidth, + maxHeight, + imageQuality, + ), + ); + + return Future.wait(resized); } /// Injects a file input with the specified accept+capture attributes, and @@ -244,17 +267,17 @@ class ImagePickerPlugin extends ImagePickerPlatform { input.onChange.first.then((event) { final files = _handleOnChangeEvent(event); if (!_completer.isCompleted && files != null) { - _completer.complete(files - .map((file) => XFile( - html.Url.createObjectUrl(file), - name: file.name, - length: file.size, - lastModified: DateTime.fromMillisecondsSinceEpoch( - file.lastModified ?? DateTime.now().millisecondsSinceEpoch, - ), - mimeType: file.type, - )) - .toList()); + _completer.complete(files.map((file) { + return XFile( + html.Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch, + ), + mimeType: file.type, + ); + }).toList()); } }); input.onError.first.then((event) { diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart new file mode 100644 index 000000000000..6ee7c5f015e2 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.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 'dart:async'; +import 'dart:math'; +import 'dart:ui'; +import 'package:image_picker_for_web/src/image_resizer_utils.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'dart:html' as html; + +/// Helper class that resizes images. +class ImageResizer { + /// Resizes the image if needed. + /// (Does not support gif images) + Future resizeImageIfNeeded(XFile file, double? maxWidth, + double? maxHeight, int? imageQuality) async { + if (!imageResizeNeeded(maxWidth, maxHeight, imageQuality) || + file.mimeType == "image/gif") { + // Implement maxWidth and maxHeight for image/gif + return file; + } + try { + final imageElement = await loadImage(file.path); + final canvas = resizeImageElement(imageElement, maxWidth, maxHeight); + final resizedImage = await writeCanvasToFile(file, canvas, imageQuality); + html.Url.revokeObjectUrl(file.path); + return resizedImage; + } catch (e) { + return file; + } + } + + /// function that loads the blobUrl into an imageElement + Future loadImage(String blobUrl) { + final imageLoadCompleter = Completer(); + final imageElement = html.ImageElement(); + imageElement.src = blobUrl; + + imageElement.onLoad.listen((event) { + imageLoadCompleter.complete(imageElement); + }); + imageElement.onError.listen((event) { + final exception = ("Error while loading image."); + imageElement.remove(); + imageLoadCompleter.completeError(exception); + }); + return imageLoadCompleter.future; + } + + /// Draws image to a canvas while resizing the image to fit the [maxWidth],[maxHeight] constraints + html.CanvasElement resizeImageElement( + html.ImageElement source, double? maxWidth, double? maxHeight) { + final newImageSize = calculateSizeOfDownScaledImage( + Size(source.width!.toDouble(), source.height!.toDouble()), + maxWidth, + maxHeight); + final canvas = html.CanvasElement(); + canvas.width = newImageSize.width.toInt(); + canvas.height = newImageSize.height.toInt(); + final context = canvas.context2D; + if (maxHeight == null && maxWidth == null) { + context.drawImage(source, 0, 0); + } else { + context.drawImageScaled(source, 0, 0, canvas.width!, canvas.height!); + } + return canvas; + } + + /// function that converts a canvas element to Xfile + /// [imageQuality] is only supported for jpeg and webp images. + Future writeCanvasToFile( + XFile originalFile, html.CanvasElement canvas, int? imageQuality) async { + final calculatedImageQuality = ((min(imageQuality ?? 100, 100)) / 100.0); + final blob = + await canvas.toBlob(originalFile.mimeType, calculatedImageQuality); + return XFile(html.Url.createObjectUrlFromBlob(blob), + mimeType: originalFile.mimeType, + name: "scaled_" + originalFile.name, + lastModified: DateTime.now(), + length: blob.size); + } +} diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart new file mode 100644 index 000000000000..6ef789254b3f --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.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 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +///a function that checks if an image needs to be resized or not +bool imageResizeNeeded(double? maxWidth, double? maxHeight, int? imageQuality) { + return imageQuality != null + ? isImageQualityValid(imageQuality) + : (maxWidth != null || maxHeight != null); +} + +/// a function that checks if image quality is between 0 to 100 +bool isImageQualityValid(int imageQuality) { + return (imageQuality >= 0 && imageQuality <= 100); +} + +/// a function that calculates the size of the downScaled image. +/// imageWidth is the width of the image +/// imageHeight is the height of the image +/// maxWidth is the maximum width of the scaled image +/// maxHeight is the maximum height of the scaled image +Size calculateSizeOfDownScaledImage( + Size imageSize, double? maxWidth, double? maxHeight) { + double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1; + double heightFactor = maxHeight != null ? imageSize.height / maxHeight : 1; + double resizeFactor = max(widthFactor, heightFactor); + return (resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize); +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 895486f3de06..d0d97011c9ec 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.3 +version: 2.1.4 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart new file mode 100644 index 000000000000..352d2bea48a5 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:ui'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_for_web/src/image_resizer_utils.dart'; + +void main() { + group('Image Resizer Utils', () { + group("calculateSizeOfScaledImage", () { + test( + "scaled image height and width are same if max width and max height are same as image's width and height", + () { + expect(calculateSizeOfDownScaledImage(Size(500, 300), 500, 300), + Size(500, 300)); + }); + + test( + "scaled image height and width are same if max width and max height are null", + () { + expect(calculateSizeOfDownScaledImage(Size(500, 300), null, null), + Size(500, 300)); + }); + + test("image size is scaled when maxWidth is set", () { + final imageSize = Size(500, 300); + final maxWidth = 400; + final scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), maxWidth.toDouble(), null); + expect(scaledSize.height <= imageSize.height, true); + expect(scaledSize.width <= maxWidth, true); + }); + + test("image size is scaled when maxHeight is set", () { + final imageSize = Size(500, 300); + final maxHeight = 400; + final scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), + null, + maxHeight.toDouble()); + expect(scaledSize.height <= maxHeight, true); + expect(scaledSize.width <= imageSize.width, true); + }); + + test("image size is scaled when both maxWidth and maxHeight is set", () { + final imageSize = Size(1120, 2000); + final maxHeight = 1200; + final maxWidth = 99; + final scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), + maxWidth.toDouble(), + maxHeight.toDouble()); + expect(scaledSize.height <= maxHeight, true); + expect(scaledSize.width <= maxWidth, true); + }); + }); + group("imageResizeNeeded", () { + test("image needs to be resized when maxWidth is set", () { + expect(imageResizeNeeded(50, null, null), true); + }); + + test("image needs to be resized when maxHeight is set", () { + expect(imageResizeNeeded(null, 50, null), true); + }); + + test("image needs to be resized when imageQuality is set", () { + expect(imageResizeNeeded(null, null, 100), true); + }); + + test("image will not be resized when imageQuality is not valid", () { + expect(imageResizeNeeded(null, null, 101), false); + expect(imageResizeNeeded(null, null, -1), false); + }); + }); + + group("isImageQualityValid", () { + test("image quality is valid in 0 to 100", () { + expect(isImageQualityValid(50), true); + expect(isImageQualityValid(0), true); + expect(isImageQualityValid(100), true); + }); + + test( + "image quality is not valid when imageQuality is less than 0 or greater than 100", + () { + expect(isImageQualityValid(-1), false); + expect(isImageQualityValid(101), false); + }); + }); + }); +} From fbb7d3aa514be062407c5b9399c3acf93d79dec9 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 19 Oct 2021 10:48:42 -0700 Subject: [PATCH 103/259] [ci] Replace Firebase Test Lab deprecated Pixel 4 device with Pixel 5 (#4436) --- .cirrus.yml | 2 +- script/tool/CHANGELOG.md | 1 + .../lib/src/firebase_test_lab_command.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 34 +++++++++---------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index ef6b9c1b6d44..cb6d0dfd63fc 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -214,7 +214,7 @@ task: - export CIRRUS_COMMIT_MESSAGE="" - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml + - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 2e6404e2cee4..3b4ff8f226d8 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +- Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't have unit tests, rather than skipping them. - Added a new `federation-safety-check` command to help catch changes to diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 941cba3a6945..28afc638203f 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -54,7 +54,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { splitCommas: false, defaultsTo: [ 'model=walleye,version=26', - 'model=flame,version=29' + 'model=redfin,version=30' ], help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e39ccf30b136..268210d00425 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -101,7 +101,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -142,7 +142,7 @@ void main() { '/packages/plugin1/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin1/example'), ProcessCall( @@ -156,7 +156,7 @@ void main() { '/packages/plugin2/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin2/example'), ]), @@ -176,7 +176,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -219,7 +219,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ProcessCall( @@ -229,7 +229,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -257,7 +257,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -294,7 +294,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -332,7 +332,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -366,7 +366,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -400,7 +400,7 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -445,7 +445,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' .split(' '), '/packages/plugin/example'), ]), @@ -468,7 +468,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -505,7 +505,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -543,7 +543,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -571,7 +571,7 @@ void main() { await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--test-run-id', 'testRunId', '--build-id', @@ -601,7 +601,7 @@ void main() { '/packages/plugin/example/android'), ProcessCall( 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), From 3bffbf87fe9688f73ba965e4509ba3c404eb1894 Mon Sep 17 00:00:00 2001 From: Lukas Kurz Date: Tue, 19 Oct 2021 22:33:05 +0200 Subject: [PATCH 104/259] [image_picker][android] suppress unchecked warning (#4408) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ .../io/flutter/plugins/imagepicker/ImagePickerDelegate.java | 1 + packages/image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4c89be1c3e48..90829cc7ae6a 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4+3 + +* Suppress a unchecked cast build warning. + ## 0.8.4+2 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index a60c1f173041..dddf67e6a382 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -217,6 +217,7 @@ void saveStateBeforeResult() { void retrieveLostImage(MethodChannel.Result result) { Map resultMap = cache.getCacheMap(); + @SuppressWarnings("unchecked") ArrayList pathList = (ArrayList) resultMap.get(cache.MAP_KEY_PATH_LIST); ArrayList newPathList = new ArrayList<>(); if (pathList != null) { diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index ba5ce6635ed6..99766702cfdf 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/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.4+2 +version: 0.8.4+3 environment: sdk: ">=2.14.0 <3.0.0" From fecd22e1de559efd62feb99141d7c4984b77afa0 Mon Sep 17 00:00:00 2001 From: James McIntosh Date: Wed, 20 Oct 2021 13:47:22 +1300 Subject: [PATCH 105/259] [google_maps_flutter] Clean Java test, consolidate Marker example. (#4400) * Do not mock internal class * Merge "Marker" examples into a single page. --- .../googlemaps/MarkersControllerTest.java | 10 +- .../example/lib/drag_marker.dart | 156 ----------- .../google_maps_flutter/example/lib/main.dart | 2 - .../example/lib/place_marker.dart | 246 +++++++++--------- 4 files changed, 131 insertions(+), 283 deletions(-) delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index afe91d77e2be..3ca78e7674d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -9,7 +9,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import com.google.android.gms.internal.maps.zzt; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; @@ -35,8 +34,7 @@ public void controller_OnMarkerDragStart() { final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); - final zzt z = mock(zzt.class); - final Marker marker = new Marker(z); + final Marker marker = mock(Marker.class); final String googleMarkerId = "abc123"; @@ -69,8 +67,7 @@ public void controller_OnMarkerDragEnd() { final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); - final zzt z = mock(zzt.class); - final Marker marker = new Marker(z); + final Marker marker = mock(Marker.class); final String googleMarkerId = "abc123"; @@ -103,8 +100,7 @@ public void controller_OnMarkerDrag() { final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); - final zzt z = mock(zzt.class); - final Marker marker = new Marker(z); + final Marker marker = mock(Marker.class); final String googleMarkerId = "abc123"; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart deleted file mode 100644 index 2c7929115247..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart +++ /dev/null @@ -1,156 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -import 'page.dart'; - -class DragMarkerPage extends GoogleMapExampleAppPage { - DragMarkerPage() : super(const Icon(Icons.drag_handle), 'Drag marker'); - - @override - Widget build(BuildContext context) { - return const DragMarkerBody(); - } -} - -class DragMarkerBody extends StatefulWidget { - const DragMarkerBody(); - - @override - State createState() => DragMarkerBodyState(); -} - -typedef MarkerUpdateAction = Marker Function(Marker marker); - -class DragMarkerBodyState extends State { - DragMarkerBodyState(); - static const LatLng center = LatLng(-33.86711, 151.1947171); - - GoogleMapController? controller; - Map markers = {}; - MarkerId? selectedMarker; - int _markerIdCounter = 1; - LatLng? markerPosition; - - void _onMapCreated(GoogleMapController controller) { - this.controller = controller; - } - - void _onMarkerTapped(MarkerId markerId) { - final Marker? tappedMarker = markers[markerId]; - if (tappedMarker != null) { - setState(() { - if (markers.containsKey(selectedMarker)) { - final Marker resetOld = markers[selectedMarker]! - .copyWith(iconParam: BitmapDescriptor.defaultMarker); - markers[selectedMarker!] = resetOld; - } - selectedMarker = markerId; - final Marker newMarker = tappedMarker.copyWith( - iconParam: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueGreen, - ), - ); - markers[markerId] = newMarker; - }); - } - } - - void _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { - setState(() { - this.markerPosition = newPosition; - }); - } - - void _add() { - final int markerCount = markers.length; - - if (markerCount == 12) { - return; - } - - final String markerIdVal = 'marker_id_$_markerIdCounter'; - _markerIdCounter++; - final MarkerId markerId = MarkerId(markerIdVal); - - final Marker marker = Marker( - draggable: true, - markerId: markerId, - position: LatLng( - center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, - center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, - ), - infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), - onTap: () => _onMarkerTapped(markerId), - onDrag: (LatLng position) => _onMarkerDrag(markerId, position), - ); - - setState(() { - markers[markerId] = marker; - }); - } - - void _remove() { - setState(() { - if (markers.containsKey(selectedMarker)) { - markers.remove(selectedMarker); - } - }); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Center( - child: GoogleMap( - onMapCreated: _onMapCreated, - initialCameraPosition: const CameraPosition( - target: center, - zoom: 11.0, - ), - markers: markers.values.toSet(), - ), - ), - ), - Container( - height: 30, - padding: EdgeInsets.only(left: 12, right: 12), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - markerPosition == null - ? Container() - : Expanded(child: Text("lat: ${markerPosition!.latitude}")), - markerPosition == null - ? Container() - : Expanded(child: Text("lng: ${markerPosition!.longitude}")), - ], - ), - ), - Row( - children: [ - TextButton( - child: const Text('add'), - onPressed: _add, - ), - TextButton( - child: const Text('remove'), - onPressed: _remove, - ), - ], - ), - ], - ); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 16f242c9e0ce..15b14db0357a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:google_maps_flutter_example/drag_marker.dart'; import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; import 'map_click.dart'; @@ -35,7 +34,6 @@ final List _allPages = [ PlacePolylinePage(), PlacePolygonPage(), PlaceCirclePage(), - DragMarkerPage(), PaddingPage(), SnapshotPage(), LiteModePage(), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 53f553eb67f8..4b9496f69a63 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -40,6 +40,7 @@ class PlaceMarkerBodyState extends State { Map markers = {}; MarkerId? selectedMarker; int _markerIdCounter = 1; + LatLng? markerPosition; void _onMapCreated(GoogleMapController controller) { this.controller = controller; @@ -67,13 +68,24 @@ class PlaceMarkerBodyState extends State { ), ); markers[markerId] = newMarker; + + markerPosition = null; }); } } + void _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + this.markerPosition = newPosition; + }); + } + void _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { + setState(() { + this.markerPosition = null; + }); await showDialog( context: context, builder: (BuildContext context) { @@ -115,12 +127,9 @@ class PlaceMarkerBodyState extends State { center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, ), infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), - onTap: () { - _onMarkerTapped(markerId); - }, - onDragEnd: (LatLng position) { - _onMarkerDragEnd(markerId, position); - }, + onTap: () => _onMarkerTapped(markerId), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), ); setState(() { @@ -280,14 +289,12 @@ class PlaceMarkerBodyState extends State { @override Widget build(BuildContext context) { final MarkerId? selectedId = selectedMarker; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: SizedBox( - width: 300.0, - height: 200.0, + return Stack(children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( child: GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: const CameraPosition( @@ -297,111 +304,114 @@ class PlaceMarkerBodyState extends State { markers: Set.of(markers.values), ), ), - ), - Expanded( - child: SingleChildScrollView( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - children: [ - Column( - children: [ - TextButton( - child: const Text('add'), - onPressed: _add, - ), - TextButton( - child: const Text('remove'), - onPressed: selectedId == null - ? null - : () => _remove(selectedId), - ), - TextButton( - child: const Text('change info'), - onPressed: selectedId == null - ? null - : () => _changeInfo(selectedId), - ), - TextButton( - child: const Text('change info anchor'), - onPressed: selectedId == null - ? null - : () => _changeInfoAnchor(selectedId), - ), - ], - ), - Column( - children: [ - TextButton( - child: const Text('change alpha'), - onPressed: selectedId == null - ? null - : () => _changeAlpha(selectedId), - ), - TextButton( - child: const Text('change anchor'), - onPressed: selectedId == null - ? null - : () => _changeAnchor(selectedId), - ), - TextButton( - child: const Text('toggle draggable'), - onPressed: selectedId == null - ? null - : () => _toggleDraggable(selectedId), - ), - TextButton( - child: const Text('toggle flat'), - onPressed: selectedId == null - ? null - : () => _toggleFlat(selectedId), - ), - TextButton( - child: const Text('change position'), - onPressed: selectedId == null - ? null - : () => _changePosition(selectedId), - ), - TextButton( - child: const Text('change rotation'), - onPressed: selectedId == null - ? null - : () => _changeRotation(selectedId), - ), - TextButton( - child: const Text('toggle visible'), - onPressed: selectedId == null - ? null - : () => _toggleVisible(selectedId), - ), - TextButton( - child: const Text('change zIndex'), - onPressed: selectedId == null - ? null - : () => _changeZIndex(selectedId), - ), - TextButton( - child: const Text('set marker icon'), - onPressed: selectedId == null - ? null - : () { - _getAssetIcon(context).then( - (BitmapDescriptor icon) { - _setMarkerIcon(selectedId, icon); - }, - ); - }, - ), - ], - ), - ], - ) - ], - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text('Add'), + onPressed: _add, + ), + TextButton( + child: const Text('Remove'), + onPressed: + selectedId == null ? null : () => _remove(selectedId), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text('change info'), + onPressed: + selectedId == null ? null : () => _changeInfo(selectedId), + ), + TextButton( + child: const Text('change info anchor'), + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), + ), + TextButton( + child: const Text('change alpha'), + onPressed: + selectedId == null ? null : () => _changeAlpha(selectedId), + ), + TextButton( + child: const Text('change anchor'), + onPressed: + selectedId == null ? null : () => _changeAnchor(selectedId), + ), + TextButton( + child: const Text('toggle draggable'), + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), + ), + TextButton( + child: const Text('toggle flat'), + onPressed: + selectedId == null ? null : () => _toggleFlat(selectedId), + ), + TextButton( + child: const Text('change position'), + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), + ), + TextButton( + child: const Text('change rotation'), + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), + ), + TextButton( + child: const Text('toggle visible'), + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), + ), + TextButton( + child: const Text('change zIndex'), + onPressed: + selectedId == null ? null : () => _changeZIndex(selectedId), + ), + TextButton( + child: const Text('set marker icon'), + onPressed: selectedId == null + ? null + : () { + _getAssetIcon(context).then( + (BitmapDescriptor icon) { + _setMarkerIcon(selectedId, icon); + }, + ); + }, + ), + ], + ), + ], + ), + Visibility( + visible: markerPosition != null, + child: Container( + color: Colors.white70, + height: 30, + padding: EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + markerPosition == null + ? Container() + : Expanded(child: Text("lat: ${markerPosition!.latitude}")), + markerPosition == null + ? Container() + : Expanded(child: Text("lng: ${markerPosition!.longitude}")), + ], ), ), - ], - ); + ), + ]); } } From 9a7895301acc8b6464329f626441fd6e61767d9c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Oct 2021 11:03:04 -0400 Subject: [PATCH 106/259] [flutter_plugin_tools] Fix license-check on Windows (#4425) --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/license_check_command.dart | 10 ++++- .../tool/test/license_check_command_test.dart | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 3b4ff8f226d8..bb4ca2390d78 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -13,6 +13,7 @@ - `license-check` now validates Kotlin files. - `pubspec-check` now checks that the description is of the pub-recommended length. +- Fix `license-check` when run on Windows with line ending conversion enabled. ## 0.7.1 diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 7165e985c059..d2c129ff7b48 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -206,7 +206,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in codeFiles) { print('Checking ${file.path}'); - final String content = await file.readAsString(); + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String content = + (await file.readAsString()).replaceAll('\r\n', '\n'); final String firstParyLicense = firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? @@ -244,7 +247,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in files) { print('Checking ${file.path}'); - if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); + if (!contents.contains(_fullBsdLicenseText)) { incorrectLicenseFiles.add(file); } } diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 5a8a90e9a674..e97274afd09e 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -48,12 +48,14 @@ void main() { 'Use of this source code is governed by a BSD-style license that can be', 'found in the LICENSE file.', ], + bool useCrlf = false, }) { final List lines = ['$prefix$comment$copyright']; for (final String line in license) { lines.add('$comment$line'); } - file.writeAsStringSync(lines.join('\n') + suffix + '\n'); + final String newline = useCrlf ? '\r\n' : '\n'; + file.writeAsStringSync(lines.join(newline) + suffix + newline); } test('looks at only expected extensions', () async { @@ -140,6 +142,23 @@ void main() { ])); }); + test('passes correct license blocks on Windows', () async { + final File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked, useCrlf: true); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check a file. + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); + }); + test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); @@ -406,6 +425,24 @@ void main() { ])); }); + test('passes correct LICENSE files on Windows', () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license + .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); + }); + test('fails if any first-party LICENSE files are incorrectly formatted', () async { final File license = root.childFile('LICENSE'); From fece8dcb1dbf756af15eb7193cfd393af9f764b0 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Oct 2021 11:08:07 -0400 Subject: [PATCH 107/259] [ci] Always run all `format` steps (#4427) --- .cirrus.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index cb6d0dfd63fc..f223ce93dfd2 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -69,8 +69,8 @@ task: - dart pub run test - name: publishable env: - # TODO (mvanbeusekom): Temporary override to "stable" because of failure on "master". - # Remove override once https://github.com/dart-lang/pub/issues/3152 is resolved. + # TODO (mvanbeusekom): Temporary override to "stable" because of failure on "master". + # Remove override once https://github.com/dart-lang/pub/issues/3152 is resolved. CHANNEL: stable CHANGE_DESC: "$TMPDIR/change-description.txt" version_check_script: @@ -88,9 +88,10 @@ task: - fi publish_check_script: ./script/tool_runner.sh publish-check - name: format - format_script: ./script/tool_runner.sh format --fail-on-change - pubspec_script: ./script/tool_runner.sh pubspec-check - license_script: dart $PLUGIN_TOOL license-check + always: + format_script: ./script/tool_runner.sh format --fail-on-change + pubspec_script: ./script/tool_runner.sh pubspec-check + license_script: dart $PLUGIN_TOOL license-check - name: federated_safety # This check is only meaningful for PRs, as it validates changes # rather than state. From b8aea6d0e73d57f5e9656f35f998d55679598871 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 20 Oct 2021 19:23:06 +0200 Subject: [PATCH 108/259] [camera] Run iOS methods on UI thread by default (#4140) --- packages/camera/camera/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 104 +++++---- .../ios/RunnerTests/CameraFocusTests.m | 12 +- .../RunnerTests/CameraMethodChannelTests.m | 48 ++++ .../ios/RunnerTests/CameraOrientationTests.m | 47 ++-- .../ios/RunnerTests/CameraPreviewPauseTests.m | 38 ++-- .../MockFLTThreadSafeFlutterResult.h | 25 +++ .../MockFLTThreadSafeFlutterResult.m | 27 +++ .../ThreadSafeFlutterResultTests.m | 122 +++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 205 +++++++++--------- .../camera/ios/Classes/CameraPlugin_Test.h | 15 ++ .../ios/Classes/FLTThreadSafeFlutterResult.h | 51 +++++ .../ios/Classes/FLTThreadSafeFlutterResult.m | 58 +++++ .../camera/ios/Classes/camera-umbrella.h | 1 + packages/camera/camera/pubspec.yaml | 2 +- 15 files changed, 560 insertions(+), 199 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m create mode 100644 packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h create mode 100644 packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m create mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index dc83a4fabca2..e6495a8fc796 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+3 + +* Fix registerTexture and result being called on background thread on iOS. + ## 0.9.4+2 * Updated package description; diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 8520bb00fb2f..feb789f2ecba 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,8 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; + 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 */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -16,9 +20,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,20 +48,22 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 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 = ""; }; - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 89D82918721FABF772705DB0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -67,9 +72,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,7 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */, + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +92,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */, + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -98,16 +105,20 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, ); path = RunnerTests; sourceTree = ""; }; - 78D1009194BD06C03BED950D /* Frameworks */ = { + 3242FD2B467C15C62200632F /* Frameworks */ = { isa = PBXGroup; children = ( - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */, - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */, + 89D82918721FABF772705DB0 /* libPods-Runner.a */, + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -131,7 +142,7 @@ 03BB76692665316900CE5A93 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, FD386F00E98D73419C929072 /* Pods */, - 78D1009194BD06C03BED950D /* Frameworks */, + 3242FD2B467C15C62200632F /* Frameworks */, ); sourceTree = ""; }; @@ -171,10 +182,10 @@ FD386F00E98D73419C929072 /* Pods */ = { isa = PBXGroup; children = ( - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */, - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */, - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */, - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */, + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */, + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */, + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */, + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -186,7 +197,7 @@ isa = PBXNativeTarget; buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */, + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */, 03BB76642665316900CE5A93 /* Sources */, 03BB76652665316900CE5A93 /* Frameworks */, 03BB76662665316900CE5A93 /* Resources */, @@ -205,7 +216,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */, + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -282,7 +293,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */ = { + 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"; + }; + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -297,28 +322,28 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(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 */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */ = { + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -333,27 +358,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -361,8 +372,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -409,7 +423,7 @@ /* Begin XCBuildConfiguration section */ 03BB766F2665316900CE5A93 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -419,6 +433,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -434,7 +449,7 @@ }; 03BB76702665316900CE5A93 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -444,6 +459,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -567,6 +583,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -588,6 +605,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m index 27537e7ebdac..fdc2be9901a4 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -19,7 +19,7 @@ @interface FLTCam : NSObject +#import "MockFLTThreadSafeFlutterResult.h" + +@interface CameraMethodChannelTests : XCTestCase +@end + +@implementation CameraMethodChannelTests + +- (void)testCreate_ShouldCallResultOnMainThread { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + // Set up mocks for initWithCameraName method + id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) + .andReturn([AVCaptureInput alloc]); + + id avCaptureSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); + OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + + // Set up method call + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; + + [camera handleMethodCallAsync:call result:resultObject]; + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; + XCTAssertNotNil(dictionaryResult); + XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index 246eab90a919..efd65a47dff8 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -10,46 +10,52 @@ #import @interface CameraOrientationTests : XCTestCase -@property(strong, nonatomic) id mockMessenger; -@property(strong, nonatomic) CameraPlugin *cameraPlugin; @end @implementation CameraOrientationTests -- (void)setUp { - [super setUp]; - - self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger]; -} - - (void)testOrientationNotifications { - id mockMessenger = self.mockMessenger; + id mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:mockMessenger]; + [mockMessenger setExpectationOrderMatters:YES]; - [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown"]; - [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp"]; - [self rotate:UIDeviceOrientationLandscapeRight expectedChannelOrientation:@"landscapeLeft"]; - [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight"]; + [self rotate:UIDeviceOrientationPortraitUpsideDown + expectedChannelOrientation:@"portraitDown" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationPortrait + expectedChannelOrientation:@"portraitUp" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeRight + expectedChannelOrientation:@"landscapeLeft" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeLeft + expectedChannelOrientation:@"landscapeRight" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); // No notification when flat. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceUp]]; // No notification when facedown. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceDown]]; OCMVerifyAll(mockMessenger); } - (void)rotate:(UIDeviceOrientation)deviceOrientation - expectedChannelOrientation:(NSString *)channelOrientation { - id mockMessenger = self.mockMessenger; + expectedChannelOrientation:(NSString *)channelOrientation + cameraPlugin:(CameraPlugin *)cameraPlugin + messenger:(NSObject *)messenger { XCTestExpectation *orientationExpectation = [self expectationWithDescription:channelOrientation]; - OCMExpect([mockMessenger + OCMExpect([messenger sendOnChannel:[OCMArg any] message:[OCMArg checkWithBlock:^BOOL(NSData *data) { NSObject *codec = [FlutterStandardMethodCodec sharedInstance]; @@ -60,8 +66,7 @@ - (void)rotate:(UIDeviceOrientation)deviceOrientation [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; }]]); - [self.cameraPlugin - orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; } diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m index 549b40a52e46..eb6c0079322c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -6,45 +6,37 @@ @import XCTest; @import AVFoundation; #import +#import "MockFLTThreadSafeFlutterResult.h" @interface FLTCam : NSObject @property(assign, nonatomic) BOOL isPreviewPaused; -- (void)pausePreviewWithResult:(FlutterResult)result; -- (void)resumePreviewWithResult:(FlutterResult)result; + +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; + +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; @end @interface CameraPreviewPauseTests : XCTestCase -@property(readonly, nonatomic) FLTCam* camera; @end @implementation CameraPreviewPauseTests -- (void)setUp { - _camera = [[FLTCam alloc] init]; -} - - (void)testPausePreviewWithResult_shouldPausePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera pausePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertTrue(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera pausePreviewWithResult:resultObject]; + XCTAssertTrue(camera.isPreviewPaused); } - (void)testResumePreviewWithResult_shouldResumePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera resumePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertFalse(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera resumePreviewWithResult:resultObject]; + XCTAssertFalse(camera.isPreviewPaused); } @end diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..8685f3fd610b --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h @@ -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. + +#ifndef MockFLTThreadSafeFlutterResult_h +#define MockFLTThreadSafeFlutterResult_h + +/** + * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and + * read the received result. + */ +@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult +@property(readonly, nonatomic, nonnull) XCTestExpectation *expectation; +@property(nonatomic, nullable) id receivedResult; + +/** + * Initializes the MockFLTThreadSafeFlutterResult with an expectation. + * + * The expectation is fullfilled when a result is called allowing tests to await the result in an + * asynchronous manner. + */ +- (nonnull instancetype)initWithExpectation:(nonnull XCTestExpectation *)expectation; +@end + +#endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..da2fc2d936ba --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m @@ -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 camera; +@import XCTest; + +#import "MockFLTThreadSafeFlutterResult.h" + +@implementation MockFLTThreadSafeFlutterResult + +- (instancetype)initWithExpectation:(XCTestExpectation *)expectation { + self = [super init]; + _expectation = expectation; + return self; +} + +- (void)sendSuccessWithData:(id)data { + self.receivedResult = data; + [self.expectation fulfill]; +} + +- (void)sendSuccess { + self.receivedResult = nil; + [self.expectation fulfill]; +} +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m new file mode 100644 index 000000000000..8cd4b8bc8c2a --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m @@ -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 camera; +@import XCTest; + +@interface ThreadSafeFlutterResultTests : XCTestCase +@end + +@implementation ThreadSafeFlutterResultTests +- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccess]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + [threadSafeFlutterResult sendSuccess]; + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterMethodNotImplemented.class]); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendNotImplemented]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult { + NSString* errorCode = @"errorCode"; + NSString* errorMessage = @"message"; + NSString* errorDetails = @"error details"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + XCTAssertEqualObjects(error.code, errorCode); + XCTAssertEqualObjects(error.message, errorMessage); + XCTAssertEqualObjects(error.details, errorDetails); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNSError_ShouldSendErrorToFlutterResult { + NSError* originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil]; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + NSString* constructedErrorCode = + [NSString stringWithFormat:@"Error %d", (int)originalError.code]; + XCTAssertEqualObjects(error.code, constructedErrorCode); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendError:originalError]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendResult_ShouldSendResultToFlutterResult { + NSString* resultData = @"resultData"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssertEqualObjects(result, resultData); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccessWithData:resultData]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 78c5a34ab05a..2c12081da807 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,16 +10,11 @@ #import #import #import - -static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} +#import "FLTThreadSafeFlutterResult.h" @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) FlutterResult result; +@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; @end @interface FLTImageStreamHandler : NSObject @@ -45,7 +40,7 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path result:(FlutterResult)result { +- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; @@ -62,7 +57,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(10)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result sendError:error]; return; } @@ -74,10 +69,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [data writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result sendSuccessWithData:_path]; } - (void)captureOutput:(AVCapturePhotoOutput *)output @@ -85,7 +80,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(11.0)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result sendError:error]; return; } @@ -93,10 +88,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [photoData writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result sendSuccessWithData:_path]; } @end @@ -460,7 +455,7 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation } } -- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { +- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; if (_resolutionPreset == max) { [settings setHighResolutionPhotoEnabled:YES]; @@ -476,7 +471,7 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { prefix:@"CAP_" error:error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; return; } @@ -816,7 +811,7 @@ - (CVPixelBufferRef)copyPixelBuffer { return pixelBuffer; } -- (void)startVideoRecordingWithResult:(FlutterResult)result { +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (!_isRecording) { NSError *error; _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" @@ -824,11 +819,11 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { prefix:@"REC_" error:error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; return; } if (![self setupWriterForPath:_videoRecordingPath]) { - result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); + [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; return; } _isRecording = YES; @@ -837,13 +832,13 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { _audioTimeOffset = CMTimeMake(0, 1); _videoIsDisconnected = NO; _audioIsDisconnected = NO; - result(nil); + [result sendSuccess]; } else { - result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]); + [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil]; } } -- (void)stopVideoRecordingWithResult:(FlutterResult)result { +- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (_isRecording) { _isRecording = NO; @@ -851,12 +846,12 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [_videoWriter finishWritingWithCompletionHandler:^{ if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { [self updateOrientation]; - result(self->_videoRecordingPath); + [result sendSuccessWithData:self->_videoRecordingPath]; self->_videoRecordingPath = nil; } else { - result([FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); + [result sendErrorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]; } }]; } @@ -865,29 +860,29 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorResourceUnavailable userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - result(getFlutterError(error)); + [result sendError:error]; } } -- (void)pauseVideoRecordingWithResult:(FlutterResult)result { +- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; - result(nil); + [result sendSuccess]; } -- (void)resumeVideoRecordingWithResult:(FlutterResult)result { +- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = NO; - result(nil); + [result sendSuccess]; } -- (void)lockCaptureOrientationWithResult:(FlutterResult)result +- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result orientation:(NSString *)orientationStr { UIDeviceOrientation orientation; @try { orientation = getUIDeviceOrientationForString(orientationStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } @@ -896,34 +891,34 @@ - (void)lockCaptureOrientationWithResult:(FlutterResult)result [self updateOrientation]; } - result(nil); + [result sendSuccess]; } -- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { +- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { _lockedCaptureOrientation = UIDeviceOrientationUnknown; [self updateOrientation]; - result(nil); + [result sendSuccess]; } -- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FlashMode mode; @try { mode = getFlashModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } if (mode == FlashModeTorch) { if (!_captureDevice.hasTorch) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support torch mode" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]; return; } if (!_captureDevice.isTorchAvailable) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Torch mode is currently not available" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOn) { @@ -933,17 +928,17 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } else { if (!_captureDevice.hasFlash) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not have flash capabilities" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]; return; } AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); if (![_capturePhotoOutput.supportedFlashModes containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support this specific flash mode" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOff) { @@ -953,20 +948,20 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } _flashMode = mode; - result(nil); + [result sendSuccess]; } -- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { ExposureMode mode; @try { mode = getExposureModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } _exposureMode = mode; [self applyExposureMode]; - result(nil); + [result sendSuccess]; } - (void)applyExposureMode { @@ -986,17 +981,17 @@ - (void)applyExposureMode { [_captureDevice unlockForConfiguration]; } -- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FocusMode mode; @try { mode = getFocusModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } _focusMode = mode; [self applyFocusMode]; - result(nil); + [result sendSuccess]; } - (void)applyFocusMode { @@ -1036,14 +1031,14 @@ - (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureD [captureDevice unlockForConfiguration]; } -- (void)pausePreviewWithResult:(FlutterResult)result { +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result { _isPreviewPaused = true; - result(nil); + [result sendSuccess]; } -- (void)resumePreviewWithResult:(FlutterResult)result { +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result { _isPreviewPaused = false; - result(nil); + [result sendSuccess]; } - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation @@ -1071,11 +1066,11 @@ - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation return CGPointMake(x, y); } -- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { - result([FlutterError errorWithCode:@"setExposurePointFailed" - message:@"Device does not have exposure point capabilities" - details:nil]); + [result sendErrorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; @@ -1086,14 +1081,14 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; - result(nil); + [result sendSuccess]; } -- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isFocusPointOfInterestSupported) { - result([FlutterError errorWithCode:@"setFocusPointFailed" - message:@"Device does not have focus point capabilities" - details:nil]); + [result sendErrorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; @@ -1105,15 +1100,14 @@ - (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; - - result(nil); + [result sendSuccess]; } -- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { +- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { [_captureDevice lockForConfiguration:nil]; [_captureDevice setExposureTargetBias:offset completionHandler:nil]; [_captureDevice unlockForConfiguration]; - result(@(offset)); + [result sendSuccessWithData:@(offset)]; } - (void)startImageStreamWithMessenger:(NSObject *)messenger { @@ -1141,19 +1135,18 @@ - (void)stopImageStream { } } -- (void)getMaxZoomLevelWithResult:(FlutterResult)result { +- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; - result([NSNumber numberWithFloat:maxZoomFactor]); + [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]]; } -- (void)getMinZoomLevelWithResult:(FlutterResult)result { +- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; - - result([NSNumber numberWithFloat:minZoomFactor]); + [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]]; } -- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { +- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; @@ -1161,22 +1154,20 @@ - (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { NSString *errorMessage = [NSString stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", minAvailableZoomFactor, maxAvailableZoomFactor]; - FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" - message:errorMessage - details:nil]; - result(error); + + [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; return; } NSError *error = nil; if (![_captureDevice lockForConfiguration:&error]) { - result(getFlutterError(error)); + [result sendError:error]; return; } _captureDevice.videoZoomFactor = zoom; [_captureDevice unlockForConfiguration]; - result(nil); + [result sendSuccess]; } - (CGFloat)getMinAvailableZoomFactor { @@ -1303,6 +1294,7 @@ @interface CameraPlugin () @implementation CameraPlugin { dispatch_queue_t _dispatchQueue; } + + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera" @@ -1366,11 +1358,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // Invoke the plugin on another dispatch queue to avoid blocking the UI. dispatch_async(_dispatchQueue, ^{ - [self handleMethodCallAsync:call result:result]; + FLTThreadSafeFlutterResult *threadSafeResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; + + [self handleMethodCallAsync:call result:threadSafeResult]; }); } -- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { +- (void)handleMethodCallAsync:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { if ([@"availableCameras" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession @@ -1399,9 +1395,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re @"sensorOrientation" : @90, }]; } - result(reply); + [result sendSuccessWithData:reply]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { NSString *cameraName = call.arguments[@"cameraName"]; @@ -1416,24 +1412,23 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re error:&error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; } else { if (_camera) { [_camera close]; } - int64_t textureId = [_registry registerTexture:cam]; + int64_t textureId = [self.registry registerTexture:cam]; _camera = cam; - - result(@{ + [result sendSuccessWithData:@{ @"cameraId" : @(textureId), - }); + }]; } } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; - result(nil); + [result sendSuccess]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; - result(nil); + [result sendSuccess]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -1465,21 +1460,21 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re }]; [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; [_camera start]; - result(nil); + [result sendSuccess]; } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } else if ([@"dispose" isEqualToString:call.method]) { [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; - result(nil); + [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; - result(nil); + [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -1509,11 +1504,11 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } [_camera setExposurePointWithResult:result x:x y:y]; } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.minExposureTargetBias)); + [result sendSuccessWithData:@(_camera.captureDevice.minExposureTargetBias)]; } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.maxExposureTargetBias)); + [result sendSuccessWithData:@(_camera.captureDevice.maxExposureTargetBias)]; } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { - result(@(0.0)); + [result sendSuccessWithData:@(0.0)]; } else if ([@"setExposureOffset" isEqualToString:call.method]) { [_camera setExposureOffsetWithResult:result offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; @@ -1537,7 +1532,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"resumePreview" isEqualToString:call.method]) { [_camera resumePreviewWithResult:result]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index 6952ae0a4aaa..afbf6864a1f8 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -5,15 +5,30 @@ // This header is available in the Test module. Import via "@import camera.Test;" #import +#import /// Methods exposed for unit testing. @interface CameraPlugin () +/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger NS_DESIGNATED_INITIALIZER; + +/// Hide the default public constructor. - (instancetype)init NS_UNAVAILABLE; +/// Handles `FlutterMethodCall`s and ensures result is send on the main dispatch queue. +/// +/// @param call The method call command object. +/// @param result A wrapper around the `FlutterResult` callback which ensures the callback is called +/// on the main dispatch queue. +- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; + +/// Called by the @c NSNotificationManager each time the device's orientation is changed. +/// +/// @param notification @c NSNotification instance containing a reference to the `UIDevice` object +/// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..f290ca0fcd05 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +/** + * Wrapper for FlutterResult that always delivers the result on the main thread. + */ +@interface FLTThreadSafeFlutterResult : NSObject + +/** + * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. + */ +@property(readonly, nonatomic, nonnull) FlutterResult flutterResult; + +/** + * Initializes with a FlutterResult object. + * @param result The FlutterResult object that the result will be given to. + */ +- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result; + +/** + * Sends a successful result without any data. + */ +- (void)sendSuccess; + +/** + * Sends a successful result with data. + * @param data Result data that is send to the Flutter Dart side. + */ +- (void)sendSuccessWithData:(nonnull id)data; + +/** + * Sends an NSError as result + * @param error Error that will be send as FlutterError. + */ +- (void)sendError:(nonnull NSError*)error; + +/** + * Sends a FlutterError as result. + */ +- (void)sendErrorWithCode:(nonnull NSString*)code + message:(nullable NSString*)message + details:(nullable id)details; + +/** + * Sends FlutterMethodNotImplemented as result. + */ +- (void)sendNotImplemented; +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..caa4788d8dc8 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeFlutterResult.h" +#import + +@implementation FLTThreadSafeFlutterResult { +} + +- (id)initWithResult:(FlutterResult)result { + self = [super init]; + if (!self) { + return nil; + } + _flutterResult = result; + return self; +} + +- (void)sendSuccess { + [self send:nil]; +} + +- (void)sendSuccessWithData:(id)data { + [self send:data]; +} + +- (void)sendError:(NSError*)error { + [self sendErrorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; +} + +- (void)sendErrorWithCode:(NSString*)code + message:(NSString* _Nullable)message + details:(id _Nullable)details { + FlutterError* flutterError = [FlutterError errorWithCode:code message:message details:details]; + [self send:flutterError]; +} + +- (void)sendNotImplemented { + [self send:FlutterMethodNotImplemented]; +} + +/** + * Sends result to flutterResult on the main thread. + */ +- (void)send:(id _Nullable)result { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_flutterResult(result); + }); + } else { + _flutterResult(result); + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index 5c39401e6261..b0fd493b24df 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,6 +4,7 @@ #import #import +#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[]; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f68d026b206c..6b4db7cd91ec 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/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+2 +version: 0.9.4+3 environment: sdk: ">=2.14.0 <3.0.0" From cdbd62b4185ed922ef2ffa846db5ee9057b16202 Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Thu, 21 Oct 2021 02:43:03 +0530 Subject: [PATCH 109/259] [flutter_plugin_android_lifecycle] remove placeholder dart file (#4413) --- packages/flutter_plugin_android_lifecycle/CHANGELOG.md | 3 ++- .../lib/flutter_plugin_android_lifecycle.dart | 6 ------ packages/flutter_plugin_android_lifecycle/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 7e567d8cce5c..27cca016960e 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.0.4 * Updated Android lint settings. +* Remove placeholder Dart file. ## 2.0.3 diff --git a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart b/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart deleted file mode 100644 index 340b06832f19..000000000000 --- a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart +++ /dev/null @@ -1,6 +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. - -// The flutter_plugin_android_lifecycle plugin only provides a Java API -// for use by Android plugins. This plugin has no Dart code. diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index 0fc128d03e17..8535de17a51c 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. repository: https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_plugin_android_lifecycle%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" From c85fa03771caa15e7eaee536189a0fe665d662d3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 20 Oct 2021 19:53:05 -0400 Subject: [PATCH 110/259] [shared_preferences] Switch to new analysis options (#4384) --- .../shared_preferences/analysis_options.yaml | 1 - .../shared_preferences/CHANGELOG.md | 4 + .../shared_preferences_test.dart | 91 +++++++-------- .../shared_preferences/example/lib/main.dart | 12 +- .../lib/shared_preferences.dart | 5 +- .../test/shared_preferences_test.dart | 107 ++++++++++-------- .../shared_preferences_linux/CHANGELOG.md | 4 + .../shared_preferences_test.dart | 47 ++++---- .../example/lib/main.dart | 12 +- .../lib/shared_preferences_linux.dart | 37 +++--- .../shared_preferences_linux/pubspec.yaml | 2 +- .../test/shared_preferences_linux_test.dart | 18 +-- .../shared_preferences_macos/CHANGELOG.md | 4 + .../shared_preferences_test.dart | 37 +++--- .../example/lib/main.dart | 8 +- .../example/pubspec.yaml | 2 +- .../CHANGELOG.md | 4 + .../method_channel_shared_preferences.dart | 4 +- ...ethod_channel_shared_preferences_test.dart | 14 +-- .../shared_preferences_web/CHANGELOG.md | 4 + .../shared_preferences_web_test.dart | 4 +- .../example/lib/main.dart | 2 +- .../example/pubspec.yaml | 10 +- .../lib/shared_preferences_web.dart | 14 +-- .../shared_preferences_windows/CHANGELOG.md | 4 + .../shared_preferences_test.dart | 38 ++++--- .../example/lib/main.dart | 12 +- .../lib/shared_preferences_windows.dart | 32 +++--- .../shared_preferences_windows/pubspec.yaml | 2 +- .../test/shared_preferences_windows_test.dart | 17 +-- script/configs/custom_analysis.yaml | 1 - 31 files changed, 296 insertions(+), 257 deletions(-) delete mode 100644 packages/shared_preferences/analysis_options.yaml diff --git a/packages/shared_preferences/analysis_options.yaml b/packages/shared_preferences/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/shared_preferences/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index db12fd1829aa..5b9179664385 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.8 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index e8498f473a2c..fafdc18ec8b1 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -4,30 +4,27 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('$SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], - }; + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], - }; + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; late SharedPreferences preferences; @@ -54,36 +51,36 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); }); testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await preferences.setString(key, kTestValues['flutter.String']); - await preferences.setBool(key, kTestValues['flutter.bool']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + await preferences.setString(key, testString); + await preferences.setBool(key, testBool); + await preferences.setInt(key, testInt); + await preferences.setDouble(key, testDouble); + await preferences.setStringList(key, testList); await preferences.remove(key); expect(preferences.get('testKey'), isNull); }); testWidgets('clearing', (WidgetTester _) async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + await preferences.setString('String', testString); + await preferences.setBool('bool', testBool); + await preferences.setInt('int', testInt); + await preferences.setDouble('double', testDouble); + await preferences.setStringList('List', testList); await preferences.clear(); expect(preferences.getString('String'), null); expect(preferences.getBool('bool'), null); @@ -94,13 +91,13 @@ void main() { testWidgets('simultaneous writes', (WidgetTester _) async { final List> writes = >[]; - final int writeCount = 100; + const int writeCount = 100; for (int i = 1; i <= writeCount; i++) { writes.add(preferences.setInt('int', i)); } - List result = await Future.wait(writes, eagerError: true); + final List result = await Future.wait(writes, eagerError: true); // All writes should succeed. - expect(result.where((element) => !element), isEmpty); + expect(result.where((bool element) => !element), isEmpty); // The last write should win. expect(preferences.getInt('int'), writeCount); }); @@ -112,28 +109,22 @@ void main() { // special prefixes plus a string value expect( // prefix for lists - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + - kTestValues2['flutter.String']), + preferences.setString('String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + testString), throwsA(isA())); await preferences.reload(); expect(preferences.getString('String'), null); expect( // prefix for big integers - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + - kTestValues2['flutter.String']), + preferences.setString('String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + testString), throwsA(isA())); await preferences.reload(); expect(preferences.getString('String'), null); expect( // prefix for doubles - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + - kTestValues2['flutter.String']), + preferences.setString('String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + testString), throwsA(isA())); await preferences.reload(); expect(preferences.getString('String'), null); diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 103481a2d295..43f3c78ad920 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,14 +24,14 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - Future _prefs = SharedPreferences.getInstance(); + final Future _prefs = SharedPreferences.getInstance(); late Future _counter; Future _incrementCounter() async { @@ -39,7 +39,7 @@ class SharedPreferencesDemoState extends State { final int counter = (prefs.getInt('counter') ?? 0) + 1; setState(() { - _counter = prefs.setInt("counter", counter).then((bool success) { + _counter = prefs.setInt('counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = _prefs.then((SharedPreferences prefs) { - return (prefs.getInt('counter') ?? 0); + return prefs.getInt('counter') ?? 0; }); } @@ -57,7 +57,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 841d615262de..b0a11fc8b13a 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -50,7 +50,8 @@ class SharedPreferences { /// performance-sensitive blocks. static Future getInstance() async { if (_completer == null) { - final completer = Completer(); + final Completer completer = + Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); @@ -188,7 +189,7 @@ class SharedPreferences { assert(fromSystem != null); // Strip the flutter. prefix from the returned preferences. final Map preferencesMap = {}; - for (String key in fromSystem.keys) { + for (final String key in fromSystem.keys) { assert(key.startsWith(_prefix)); preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index 878021a03361..1938a04ae84b 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -11,27 +11,37 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + const Map testValues = { + 'flutter.String': testString, + 'flutter.bool': testBool, + 'flutter.int': testInt, + 'flutter.double': testDouble, + 'flutter.List': testList, }; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; + const Map testValues2 = { + 'flutter.String': testString2, + 'flutter.bool': testBool2, + 'flutter.int': testInt2, + 'flutter.double': testDouble2, + 'flutter.List': testList2, }; late FakeSharedPreferencesStore store; late SharedPreferences preferences; setUp(() async { - store = FakeSharedPreferencesStore(kTestValues); + store = FakeSharedPreferencesStore(testValues); SharedPreferencesStorePlatform.instance = store; preferences = await SharedPreferences.getInstance(); store.log.clear(); @@ -43,26 +53,26 @@ void main() { }); test('reading', () async { - expect(preferences.get('String'), kTestValues['flutter.String']); - expect(preferences.get('bool'), kTestValues['flutter.bool']); - expect(preferences.get('int'), kTestValues['flutter.int']); - expect(preferences.get('double'), kTestValues['flutter.double']); - expect(preferences.get('List'), kTestValues['flutter.List']); - expect(preferences.getString('String'), kTestValues['flutter.String']); - expect(preferences.getBool('bool'), kTestValues['flutter.bool']); - expect(preferences.getInt('int'), kTestValues['flutter.int']); - expect(preferences.getDouble('double'), kTestValues['flutter.double']); - expect(preferences.getStringList('List'), kTestValues['flutter.List']); + expect(preferences.get('String'), testString); + expect(preferences.get('bool'), testBool); + expect(preferences.get('int'), testInt); + expect(preferences.get('double'), testDouble); + expect(preferences.get('List'), testList); + expect(preferences.getString('String'), testString); + expect(preferences.getBool('bool'), testBool); + expect(preferences.getInt('int'), testInt); + expect(preferences.getDouble('double'), testDouble); + expect(preferences.getStringList('List'), testList); expect(store.log, []); }); test('writing', () async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) ]); expect( store.log, @@ -70,37 +80,37 @@ void main() { isMethodCall('setValue', arguments: [ 'String', 'flutter.String', - kTestValues2['flutter.String'], + testString2, ]), isMethodCall('setValue', arguments: [ 'Bool', 'flutter.bool', - kTestValues2['flutter.bool'], + testBool2, ]), isMethodCall('setValue', arguments: [ 'Int', 'flutter.int', - kTestValues2['flutter.int'], + testInt2, ]), isMethodCall('setValue', arguments: [ 'Double', 'flutter.double', - kTestValues2['flutter.double'], + testDouble2, ]), isMethodCall('setValue', arguments: [ 'StringList', 'flutter.List', - kTestValues2['flutter.List'], + testList2, ]), ], ); store.log.clear(); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); expect(store.log, equals([])); }); @@ -139,16 +149,15 @@ void main() { }); test('reloading', () async { - await preferences.setString( - 'String', kTestValues['flutter.String'] as String); - expect(preferences.getString('String'), kTestValues['flutter.String']); + await preferences.setString('String', testString); + expect(preferences.getString('String'), testString); SharedPreferences.setMockInitialValues( - kTestValues2.cast()); - expect(preferences.getString('String'), kTestValues['flutter.String']); + testValues2.cast()); + expect(preferences.getString('String'), testString); await preferences.reload(); - expect(preferences.getString('String'), kTestValues2['flutter.String']); + expect(preferences.getString('String'), testString2); }); test('back to back calls should return same instance.', () async { @@ -189,13 +198,13 @@ void main() { test('writing copy of strings list', () async { final List myList = []; - await preferences.setStringList("myList", myList); - myList.add("foobar"); + await preferences.setStringList('myList', myList); + myList.add('foobar'); final List cachedList = preferences.getStringList('myList')!; expect(cachedList, []); - cachedList.add("foobar2"); + cachedList.add('foobar2'); expect(preferences.getStringList('myList'), []); }); @@ -223,13 +232,13 @@ class FakeSharedPreferencesStore implements SharedPreferencesStorePlatform { @override Future clear() { - log.add(MethodCall('clear')); + log.add(const MethodCall('clear')); return backend.clear(); } @override Future> getAll() { - log.add(MethodCall('getAll')); + log.add(const MethodCall('getAll')); return backend.getAll(); } diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index fc09bec23591..6fd14eebc7f0 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.2 * Updated installation instructions in README. 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 5dba3def31d0..b33d6011f5c4 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 @@ -12,7 +12,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesLinux', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -20,7 +20,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -39,7 +39,7 @@ void main() { }); testWidgets('reading', (WidgetTester _) async { - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], isNull); expect(all['bool'], isNull); expect(all['int'], isNull); @@ -50,14 +50,15 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + 'String', 'String', kTestValues2['flutter.String']!), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']!), preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) + 'Double', 'double', kTestValues2['flutter.double']!), + preferences.setValue( + 'StringList', 'List', kTestValues2['flutter.List']!) ]); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], kTestValues2['flutter.String']); expect(all['bool'], kTestValues2['flutter.bool']); expect(all['int'], kTestValues2['flutter.int']); @@ -68,28 +69,30 @@ void main() { testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await Future.wait([ - preferences.setValue('String', key, kTestValues['flutter.String']), - preferences.setValue('Bool', key, kTestValues['flutter.bool']), - preferences.setValue('Int', key, kTestValues['flutter.int']), - preferences.setValue('Double', key, kTestValues['flutter.double']), - preferences.setValue('StringList', key, kTestValues['flutter.List']) + await Future.wait(>[ + preferences.setValue('String', key, kTestValues['flutter.String']!), + preferences.setValue('Bool', key, kTestValues['flutter.bool']!), + preferences.setValue('Int', key, kTestValues['flutter.int']!), + preferences.setValue('Double', key, kTestValues['flutter.double']!), + preferences.setValue('StringList', key, kTestValues['flutter.List']!) ]); await preferences.remove(key); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['testKey'], isNull); }); testWidgets('clearing', (WidgetTester _) async { await Future.wait(>[ - preferences.setValue('String', 'String', kTestValues['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues['flutter.int']), - preferences.setValue('Double', 'double', kTestValues['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues['flutter.List']) + preferences.setValue( + 'String', 'String', kTestValues['flutter.String']!), + preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!), + preferences.setValue('Int', 'int', kTestValues['flutter.int']!), + preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']!), + preferences.setValue('StringList', 'List', kTestValues['flutter.List']!) ]); await preferences.clear(); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], null); expect(all['bool'], null); expect(all['int'], null); diff --git a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart index aee71d00d44d..b4a7ea3f2c1e 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,18 +24,18 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - final prefs = SharedPreferencesLinux.instance; + final SharedPreferencesLinux prefs = SharedPreferencesLinux.instance; late Future _counter; Future _incrementCounter() async { - final values = await prefs.getAll(); + final Map values = await prefs.getAll(); final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] as int? ?? 0); + return values['counter'] as int? ?? 0; }); } @@ -57,7 +57,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index 5ec988216074..cf887ccb7ad1 100644 --- a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart @@ -17,8 +17,8 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor /// This class implements the `package:shared_preferences` functionality for Linux. class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// The default instance of [SharedPreferencesLinux] to use. - /// TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. - /// https://github.com/flutter/flutter/issues/81421 + // TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. + // https://github.com/flutter/flutter/issues/81421 static SharedPreferencesLinux instance = SharedPreferencesLinux(); /// Registers the Linux implementation. @@ -31,13 +31,15 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// File system used to store to disk. Exposed for testing only. @visibleForTesting - FileSystem fs = LocalFileSystem(); + FileSystem fs = const LocalFileSystem(); /// Gets the file where the preferences are stored. Future _getLocalDataFile() async { - final pathProvider = PathProviderLinux(); - final directory = await pathProvider.getApplicationSupportPath(); - if (directory == null) return null; + final PathProviderLinux pathProvider = PathProviderLinux(); + final String? directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; + } return fs.file(path.join(directory, 'shared_preferences.json')); } @@ -48,12 +50,15 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { return _cachedPreferences!; } - Map preferences = {}; + Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); + final String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - preferences = json.decode(stringMap).cast(); + final Object? data = json.decode(stringMap); + if (data is Map) { + preferences = data.cast(); + } } } _cachedPreferences = preferences; @@ -64,18 +69,18 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// succeeded. Future _writePreferences(Map preferences) async { try { - var localDataFile = await _getLocalDataFile(); + final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print("Unable to determine where to write preferences."); + print('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } - var stringMap = json.encode(preferences); + final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print("Error saving preferences to disk: $e"); + print('Error saving preferences to disk: $e'); return false; } return true; @@ -83,7 +88,7 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @override Future clear() async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.clear(); return _writePreferences(preferences); } @@ -95,14 +100,14 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.remove(key); return _writePreferences(preferences); } @override Future setValue(String valueType, String key, Object value) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences[key] = value; return _writePreferences(preferences); } diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index c03e49e042e2..b6d1765c08f3 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -18,9 +18,9 @@ flutter: dependencies: file: ^6.0.0 - meta: ^1.3.0 flutter: sdk: flutter + meta: ^1.3.0 path: ^1.8.0 path_provider_linux: ^2.0.0 shared_preferences_platform_interface: ^2.0.0 diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 62ec2b66c07a..40dbdd58afc1 100644 --- a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart @@ -18,12 +18,12 @@ void main() { }); Future _getFilePath() async { - final pathProvider = PathProviderLinux(); - final directory = await pathProvider.getApplicationSupportPath(); + final PathProviderLinux pathProvider = PathProviderLinux(); + final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - _writeTestFile(String value) async { + Future _writeTestFile(String value) async { fs.file(await _getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); @@ -34,7 +34,7 @@ void main() { } SharedPreferencesLinux _getPreferences() { - var prefs = SharedPreferencesLinux(); + final SharedPreferencesLinux prefs = SharedPreferencesLinux(); prefs.fs = fs; return prefs; } @@ -46,9 +46,9 @@ void main() { test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); - var values = await prefs.getAll(); + final Map values = await prefs.getAll(); expect(values, hasLength(2)); expect(values['key1'], 'one'); expect(values['key2'], 2); @@ -56,7 +56,7 @@ void main() { test('remove', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.remove('key2'); @@ -65,7 +65,7 @@ void main() { test('setValue', () async { await _writeTestFile('{}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); @@ -75,7 +75,7 @@ void main() { test('clear', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.clear(); expect(await _readTestFile(), '{}'); diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 2f7e0edf9a51..7a2e3ab74e72 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.2 * Add native unit tests. 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 e7a267f7e51a..66e3be30ee5d 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 @@ -3,15 +3,16 @@ // found in the LICENSE file. import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesMacOS', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -19,7 +20,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -55,15 +56,15 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', _prefixedKey('String'), kTestValues2['flutter.String']), + 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), preferences.setValue( - 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']), + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), preferences.setValue( - 'Int', _prefixedKey('int'), kTestValues2['flutter.int']), + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), preferences.setValue( - 'Double', _prefixedKey('double'), kTestValues2['flutter.double']), + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), preferences.setValue( - 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']) + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) ]); final Map values = await preferences.getAll(); expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); @@ -75,12 +76,12 @@ void main() { testWidgets('removing', (WidgetTester _) async { final String key = _prefixedKey('testKey'); - await preferences.setValue('String', key, kTestValues['flutter.String']); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']); - await preferences.setValue('Int', key, kTestValues['flutter.int']); - await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']); + 'StringList', key, kTestValues['flutter.List']!); await preferences.remove(key); final Map values = await preferences.getAll(); expect(values[key], isNull); @@ -88,13 +89,13 @@ void main() { testWidgets('clearing', (WidgetTester _) async { await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']); + 'Double', 'double', kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']); + 'StringList', 'List', kTestValues['flutter.List']!); await preferences.clear(); final Map values = await preferences.getAll(); expect(values['String'], null); diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart index fb85c301f623..349e9c45405a 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,14 +24,14 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - SharedPreferencesStorePlatform _prefs = + final SharedPreferencesStorePlatform _prefs = SharedPreferencesStorePlatform.instance; late Future _counter; @@ -63,7 +63,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml index e6bf972cf5b4..d6f07f8eb2af 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -9,7 +9,6 @@ environment: dependencies: flutter: sdk: flutter - shared_preferences_platform_interface: ^2.0.0 shared_preferences_macos: # When depending on this package from a real application you should use: # shared_preferences_macos: ^x.y.z @@ -17,6 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_driver: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index b402f6e57e88..51b59651b49a 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.0 * Migrate to null safety. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart index fa1bdc097b8d..2974f0a69e1b 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart @@ -43,7 +43,9 @@ class MethodChannelSharedPreferencesStore final Map? preferences = await _kChannel.invokeMapMethod('getAll'); - if (preferences == null) return {}; + if (preferences == null) { + return {}; + } return preferences; } } diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart index 3b43062c2be3..31ee89e4c3f6 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart @@ -4,8 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -37,7 +37,7 @@ void main() { return await testData.getAll(); } if (methodCall.method == 'remove') { - final String key = methodCall.arguments['key']; + final String key = (methodCall.arguments['key'] as String?)!; return await testData.remove(key); } if (methodCall.method == 'clear') { @@ -47,8 +47,8 @@ void main() { final Match? match = setterRegExp.matchAsPrefix(methodCall.method); if (match?.groupCount == 1) { final String valueType = match!.group(1)!; - final String key = methodCall.arguments['key']; - final Object value = methodCall.arguments['value']; + final String key = (methodCall.arguments['key'] as String?)!; + final Object value = (methodCall.arguments['value'] as Object?)!; return await testData.setValue(valueType, key, value); } fail('Unexpected method call: ${methodCall.method}'); @@ -78,15 +78,15 @@ void main() { }); expect(log, hasLength(4)); - for (MethodCall call in log) { + for (final MethodCall call in log) { expect(call.method, 'remove'); } }); test('setValue', () async { expect(await testData.getAll(), isEmpty); - for (String key in kTestValues.keys) { - final dynamic value = kTestValues[key]; + for (final String key in kTestValues.keys) { + final Object value = kTestValues[key]!; expect(await store.setValue(key.split('.').last, key, value), true); } expect(await testData.getAll(), kTestValues); diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index dd68f5321541..6332d01aa743 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.2 * Add `implements` to pubspec. diff --git a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart index d95a0512615e..d3bfa49af8a0 100644 --- a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart @@ -62,12 +62,12 @@ void main() { testWidgets('setValue', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - for (String key in kTestValues.keys) { + for (final String key in kTestValues.keys) { final dynamic value = kTestValues[key]; expect(await store.setValue(key.split('.').last, key, value), true); } expect(html.window.localStorage.keys, hasLength(kTestValues.length)); - for (String key in html.window.localStorage.keys) { + for (final String key in html.window.localStorage.keys) { expect(html.window.localStorage[key], json.encode(kTestValues[key])); } diff --git a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart index e1a38dcdcd46..341913a18490 100644 --- a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart @@ -17,7 +17,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index a83a71b40bf8..832ba912e5a8 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -6,16 +6,16 @@ environment: flutter: ">=2.2.0" dependencies: - shared_preferences_web: - path: ../ flutter: sdk: flutter + shared_preferences_web: + path: ../ dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter + js: ^0.6.3 diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart index 9cff1d448896..d9d623465188 100644 --- a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart +++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart @@ -23,16 +23,14 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { // IMPORTANT: Do not use html.window.localStorage.clear() as that will // remove _all_ local data, not just the keys prefixed with // "flutter." - for (String key in _storedFlutterKeys) { - html.window.localStorage.remove(key); - } + _storedFlutterKeys.forEach(html.window.localStorage.remove); return true; } @override Future> getAll() async { - final Map allData = {}; - for (String key in _storedFlutterKeys) { + final Map allData = {}; + for (final String key in _storedFlutterKeys) { allData[key] = _decodeValue(html.window.localStorage[key]!); } return allData; @@ -64,7 +62,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { Iterable get _storedFlutterKeys { return html.window.localStorage.keys - .where((key) => key.startsWith('flutter.')); + .where((String key) => key.startsWith('flutter.')); } String _encodeValue(Object? value) { @@ -72,7 +70,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } Object _decodeValue(String encodedValue) { - final Object decodedValue = json.decode(encodedValue); + final Object? decodedValue = json.decode(encodedValue); if (decodedValue is List) { // JSON does not preserve generics. The encode/decode roundtrip is @@ -81,6 +79,6 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { return decodedValue.cast(); } - return decodedValue; + return decodedValue!; } } diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 7502ec917d80..68358a3c354e 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart index 207d712650a7..ac138666518a 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart @@ -3,15 +3,16 @@ // found in the LICENSE file. import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_windows/shared_preferences_windows.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesWindows', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -19,7 +20,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -49,12 +50,13 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + 'String', 'String', kTestValues2['flutter.String']!), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']!), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']!), preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) + 'StringList', 'List', kTestValues2['flutter.List']!) ]); final Map values = await preferences.getAll(); expect(values['String'], kTestValues2['flutter.String']); @@ -66,12 +68,12 @@ void main() { testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await preferences.setValue('String', key, kTestValues['flutter.String']); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']); - await preferences.setValue('Int', key, kTestValues['flutter.int']); - await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']); + 'StringList', key, kTestValues['flutter.List']!); await preferences.remove(key); final Map values = await preferences.getAll(); expect(values[key], isNull); @@ -79,13 +81,13 @@ void main() { testWidgets('clearing', (WidgetTester _) async { await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']); + 'Double', 'double', kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']); + 'StringList', 'List', kTestValues['flutter.List']!); await preferences.clear(); final Map values = await preferences.getAll(); expect(values['String'], null); diff --git a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart index 0cdd37394706..423a727042cc 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,18 +24,18 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - final prefs = SharedPreferencesWindows.instance; + final SharedPreferencesWindows prefs = SharedPreferencesWindows.instance; late Future _counter; Future _incrementCounter() async { - final values = await prefs.getAll(); + final Map values = await prefs.getAll(); final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] as int? ?? 0); + return values['counter'] as int? ?? 0; }); } @@ -57,7 +57,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index b8cd3702b837..02f9f85d6381 100644 --- a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -4,20 +4,21 @@ import 'dart:async'; import 'dart:convert' show json; + import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; /// The Windows implementation of [SharedPreferencesStorePlatform]. /// /// This class implements the `package:shared_preferences` functionality for Windows. class SharedPreferencesWindows extends SharedPreferencesStorePlatform { /// The default instance of [SharedPreferencesWindows] to use. - /// TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. - /// https://github.com/flutter/flutter/issues/81421 + // TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. + // https://github.com/flutter/flutter/issues/81421 static SharedPreferencesWindows instance = SharedPreferencesWindows(); /// Registers the Windows implementation. @@ -27,7 +28,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { /// File system used to store to disk. Exposed for testing only. @visibleForTesting - FileSystem fs = LocalFileSystem(); + FileSystem fs = const LocalFileSystem(); /// The path_provider_windows instance used to find the support directory. @visibleForTesting @@ -44,7 +45,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { if (_localDataFilePath != null) { return _localDataFilePath!; } - final directory = await pathProvider.getApplicationSupportPath(); + final String? directory = await pathProvider.getApplicationSupportPath(); if (directory == null) { return null; } @@ -58,12 +59,15 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { if (_cachedPreferences != null) { return _cachedPreferences!; } - Map preferences = {}; + Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); + final String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - preferences = json.decode(stringMap).cast(); + final Object? data = json.decode(stringMap); + if (data is Map) { + preferences = data.cast(); + } } } _cachedPreferences = preferences; @@ -76,16 +80,16 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { try { final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print("Unable to determine where to write preferences."); + print('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } - String stringMap = json.encode(preferences); + final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print("Error saving preferences to disk: $e"); + print('Error saving preferences to disk: $e'); return false; } return true; @@ -93,7 +97,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { @override Future clear() async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.clear(); return _writePreferences(preferences); } @@ -105,14 +109,14 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.remove(key); return _writePreferences(preferences); } @override Future setValue(String valueType, String key, Object value) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences[key] = value; return _writePreferences(preferences); } diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 87b685f6d0bc..76b3c74628b4 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -17,9 +17,9 @@ flutter: pluginClass: none dependencies: + file: ^6.0.0 flutter: sdk: flutter - file: ^6.0.0 meta: ^1.3.0 path: ^1.8.0 path_provider_platform_interface: ^2.0.0 diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index 6bb21b814e07..0c47e9865765 100644 --- a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart @@ -20,11 +20,11 @@ void main() { }); Future _getFilePath() async { - final directory = await pathProvider.getApplicationSupportPath(); + final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - _writeTestFile(String value) async { + Future _writeTestFile(String value) async { fileSystem.file(await _getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); @@ -35,7 +35,7 @@ void main() { } SharedPreferencesWindows _getPreferences() { - var prefs = SharedPreferencesWindows(); + final SharedPreferencesWindows prefs = SharedPreferencesWindows(); prefs.fs = fileSystem; prefs.pathProvider = pathProvider; return prefs; @@ -49,9 +49,9 @@ void main() { test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); - var values = await prefs.getAll(); + final Map values = await prefs.getAll(); expect(values, hasLength(2)); expect(values['key1'], 'one'); expect(values['key2'], 2); @@ -59,7 +59,7 @@ void main() { test('remove', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.remove('key2'); @@ -68,7 +68,7 @@ void main() { test('setValue', () async { await _writeTestFile('{}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); @@ -78,7 +78,7 @@ void main() { test('clear', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.clear(); expect(await _readTestFile(), '{}'); @@ -92,6 +92,7 @@ void main() { /// path it returns is a root path that does not actually exist on Windows. class FakePathProviderWindows extends PathProviderPlatform implements PathProviderWindows { + @override late VersionInfoQuerier versionInfoQuerier; @override diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 71d8fb825add..3840d64f33f3 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -23,7 +23,6 @@ - ios_platform_images - local_auth - quick_actions -- shared_preferences - url_launcher - video_player - webview_flutter From 5eec1e61d40c77b1db8250c9e0a085e894c820a2 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 21 Oct 2021 12:38:50 -0700 Subject: [PATCH 111/259] [ci] Update macOS Cirrus image to Xcode 13 (#4429) In anticipation of flutter/flutter#91634 Flutter tool enforcement, and to use the latest Xcode and SDKs that our customers may be using, upgrade the macOS Cirrus image to Xcode 13. --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index f223ce93dfd2..da354c5a9b22 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -47,7 +47,7 @@ macos_template: &MACOS_TEMPLATE # PRs on macOS. use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: big-sur-xcode-12.5 + image: big-sur-xcode-13 # Light-workload Linux tasks. # These use default machines, with fewer CPUs, to reduce pressure on the @@ -275,7 +275,7 @@ task: SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-5 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-15-0 | xargs xcrun simctl boot build_script: - ./script/tool_runner.sh build-examples --ios xcode_analyze_script: From 7df7a8f71a853913cb6efac5b4ec687f6a392fdb Mon Sep 17 00:00:00 2001 From: Pavel Shapovalov Date: Fri, 22 Oct 2021 00:03:04 +0300 Subject: [PATCH 112/259] [google_sign_in] add serverAuthCode to GoogleSignInAccount (#4180) --- .../google_sign_in/google_sign_in/CHANGELOG.md | 4 ++++ .../google_sign_in/lib/google_sign_in.dart | 11 ++++++++++- .../google_sign_in/lib/src/common.dart | 3 +++ .../google_sign_in/lib/testing.dart | 5 +++++ .../google_sign_in/google_sign_in/pubspec.yaml | 4 ++-- .../google_sign_in/test/google_sign_in_test.dart | 15 +++++++++------ 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 6107560ce610..6f4ccebf8dc3 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.2.0 + +* Add `GoogleSignInAccount.serverAuthCode`. Mark `GoogleSignInAuthentication.serverAuthCode` as deprecated. + ## 5.1.1 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. 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 04d60fbc7d21..e104093c69bc 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 @@ -28,6 +28,7 @@ class GoogleSignInAuthentication { String? get accessToken => _data.accessToken; /// Server auth code used to access Google Login + @Deprecated('Use the `GoogleSignInAccount.serverAuthCode` property instead') String? get serverAuthCode => _data.serverAuthCode; @override @@ -44,6 +45,7 @@ class GoogleSignInAccount implements GoogleIdentity { email = data.email, id = data.id, photoUrl = data.photoUrl, + serverAuthCode = data.serverAuthCode, _idToken = data.idToken { assert(id != null); } @@ -68,6 +70,9 @@ class GoogleSignInAccount implements GoogleIdentity { @override final String? photoUrl; + @override + final String? serverAuthCode; + final String? _idToken; final GoogleSignIn _googleSignIn; @@ -97,6 +102,7 @@ class GoogleSignInAccount implements GoogleIdentity { if (response.idToken == null) { response.idToken = _idToken; } + return GoogleSignInAuthentication._(response); } @@ -132,11 +138,13 @@ class GoogleSignInAccount implements GoogleIdentity { email == otherAccount.email && id == otherAccount.id && photoUrl == otherAccount.photoUrl && + serverAuthCode == otherAccount.serverAuthCode && _idToken == otherAccount._idToken; } @override - int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken); + int get hashCode => + hashValues(displayName, email, id, photoUrl, _idToken, serverAuthCode); @override String toString() { @@ -145,6 +153,7 @@ class GoogleSignInAccount implements GoogleIdentity { 'email': email, 'id': id, 'photoUrl': photoUrl, + 'serverAuthCode': serverAuthCode }; return 'GoogleSignInAccount:$data'; } diff --git a/packages/google_sign_in/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/common.dart index 068403e74629..8a1d4dcb354f 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/common.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/common.dart @@ -34,4 +34,7 @@ abstract class GoogleIdentity { /// /// Not guaranteed to be present for all users, even when configured. String? get photoUrl; + + /// Server auth code used to access Google Login + String? get serverAuthCode; } diff --git a/packages/google_sign_in/google_sign_in/lib/testing.dart b/packages/google_sign_in/google_sign_in/lib/testing.dart index c4d2da3089a5..e519b34b199a 100644 --- a/packages/google_sign_in/google_sign_in/lib/testing.dart +++ b/packages/google_sign_in/google_sign_in/lib/testing.dart @@ -67,6 +67,7 @@ class FakeUser { this.email, this.displayName, this.photoUrl, + this.serverAuthCode, this.idToken, this.accessToken, }); @@ -83,6 +84,9 @@ class FakeUser { /// Will be converted into [GoogleSignInUserData.photoUrl]. final String? photoUrl; + /// Will be converted into [GoogleSignInUserData.serverAuthCode]. + final String? serverAuthCode; + /// Will be converted into [GoogleSignInTokenData.idToken]. final String? idToken; @@ -94,6 +98,7 @@ class FakeUser { 'email': email, 'displayName': displayName, 'photoUrl': photoUrl, + 'serverAuthCode': serverAuthCode, 'idToken': idToken, }; } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index aa0a686776fb..91114c6b0491 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/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 and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.1.1 +version: 5.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -23,7 +23,7 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.0.1 + google_sign_in_platform_interface: ^2.1.0 google_sign_in_web: ^0.10.0 meta: ^1.3.0 diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 444edc4336ce..76bc2d600541 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -23,6 +23,7 @@ void main() { "id": "8162538176523816253123", "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", "displayName": "John Doe", + "serverAuthCode": "789" }; const Map kDefaultResponses = { @@ -350,7 +351,8 @@ void main() { expect(auth.accessToken, '456'); expect(auth.idToken, '123'); - expect(auth.serverAuthCode, '789'); + // fix deprecated_member_use_from_same_package + // expect(auth.serverAuthCode, '789'); expect( log, [ @@ -382,11 +384,11 @@ void main() { group('GoogleSignIn with fake backend', () { const FakeUser kUserData = FakeUser( - id: "8162538176523816253123", - displayName: "John Doe", - email: "john.doe@gmail.com", - photoUrl: "https://lh5.googleusercontent.com/photo.jpg", - ); + id: "8162538176523816253123", + displayName: "John Doe", + email: "john.doe@gmail.com", + photoUrl: "https://lh5.googleusercontent.com/photo.jpg", + serverAuthCode: '789'); late GoogleSignIn googleSignIn; @@ -411,6 +413,7 @@ void main() { expect(user.email, equals(kUserData.email)); expect(user.id, equals(kUserData.id)); expect(user.photoUrl, equals(kUserData.photoUrl)); + expect(user.serverAuthCode, equals(kUserData.serverAuthCode)); await googleSignIn.disconnect(); expect(googleSignIn.currentUser, isNull); From 99fbfbd56fe4075a5622e874864fabbbef1c3881 Mon Sep 17 00:00:00 2001 From: Wyte Krongapiradee Date: Fri, 22 Oct 2021 05:08:07 +0800 Subject: [PATCH 113/259] [webview] Fix typos in the README (#4249) --- packages/webview_flutter/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/webview_flutter/README.md | 4 ++-- packages/webview_flutter/webview_flutter/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 6724b43476ff..f599889c0d5e 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.2 + +* Fix typos in the README. + ## 2.1.1 * Fixed `_CastError` that was thrown when running the example App. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index a1a98901affb..de475ad9acd7 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -23,8 +23,8 @@ Here are some points to consider when choosing between the two: * *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple [keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) -* *Hybrid composition* mode requires Android SKD 19+ while *Virtual displays* mode requires Android SDK 20+ -* *Hybrid composition* mode has [performence limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions +* *Hybrid composition* mode requires Android SDK 19+ while *Virtual displays* mode requires Android SDK 20+ +* *Hybrid composition* mode has [performance limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions | | Hybrid composition | Virtual displays | | --------------------------- | ------------------- | ---------------- | diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 4206789cb1b3..dabfe0d0d14a 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.14.0 <3.0.0" From 489c913f4eb78dfe27d0cdbfa8df77add544705e Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Oct 2021 16:58:03 -0700 Subject: [PATCH 114/259] [google_sign_in] remove the commented out code in tests (#4442) --- .../google_sign_in/google_sign_in/test/google_sign_in_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 76bc2d600541..0a019a2ffae5 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -351,8 +351,6 @@ void main() { expect(auth.accessToken, '456'); expect(auth.idToken, '123'); - // fix deprecated_member_use_from_same_package - // expect(auth.serverAuthCode, '789'); expect( log, [ From 4b6b6b24c7a5c635d256c6556f63fbe0b755a825 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 21 Oct 2021 17:17:18 -0700 Subject: [PATCH 115/259] Use OpenJDK 11 in CI jobs (#4419) --- .ci/Dockerfile | 4 ---- .../connectivity/example/android/app/build.gradle | 6 +++--- .../android/app/gradle/wrapper/gradle-wrapper.properties | 5 ----- .../connectivity/connectivity/example/android/build.gradle | 2 +- .../android/gradle/wrapper/gradle-wrapper.properties | 2 +- .../video_player/example/android/app/build.gradle | 6 +++--- .../video_player/video_player/example/android/build.gradle | 2 +- .../android/gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/.ci/Dockerfile b/.ci/Dockerfile index a3deb6948d90..60f82eb005ed 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -5,10 +5,6 @@ FROM cirrusci/flutter:2.2.2 RUN apt-get update -y -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk -ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 - RUN apt-get install -y --no-install-recommends gnupg # Add repo for gcloud sdk and install it diff --git a/packages/connectivity/connectivity/example/android/app/build.gradle b/packages/connectivity/connectivity/example/android/app/build.gradle index 64f3d0626bf4..99e360558af8 100644 --- a/packages/connectivity/connectivity/example/android/app/build.gradle +++ b/packages/connectivity/connectivity/example/android/app/build.gradle @@ -52,9 +52,9 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'org.mockito:mockito-core:3.5.13' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'org.robolectric:robolectric:3.8' - testImplementation 'org.mockito:mockito-core:3.5.13' } diff --git a/packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/connectivity/connectivity/example/android/build.gradle b/packages/connectivity/connectivity/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/connectivity/connectivity/example/android/build.gradle +++ b/packages/connectivity/connectivity/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:7.0.1' } } diff --git a/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties index 01a286e96a21..297f2fec363f 100644 --- a/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 0d1d5031ef4f..5f1f3afa6352 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -58,9 +58,9 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'org.mockito:mockito-core:3.5.13' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'org.robolectric:robolectric:3.8' - testImplementation 'org.mockito:mockito-core:3.5.13' } diff --git a/packages/video_player/video_player/example/android/build.gradle b/packages/video_player/video_player/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/video_player/video_player/example/android/build.gradle +++ b/packages/video_player/video_player/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:7.0.1' } } diff --git a/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7318..b8793d3c0d69 100644 --- a/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip From 94b836eca3654e22c73efc0a9b6946b484013c28 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 25 Oct 2021 11:29:07 -0400 Subject: [PATCH 116/259] [flutter_plugin_tools] Fix pubspec-check on Windows (#4428) The repository check always failed when run on Windows, because the relative path generated to check the end of the URL was using local filesystem style for separators, but URLs always use POSIX separators. --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 2 +- .../tool/test/pubspec_check_command_test.dart | 437 ++++++++++-------- 3 files changed, 247 insertions(+), 193 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index bb4ca2390d78..89bc7759b962 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -14,6 +14,7 @@ - `pubspec-check` now checks that the description is of the pub-recommended length. - Fix `license-check` when run on Windows with line ending conversion enabled. +- Fixed `pubspec-check` on Windows. ## 0.7.1 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index b99f5af68c45..04c3848a7dec 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -177,7 +177,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { errorMessages.add('Missing "repository"'); } else { final String relativePackagePath = - path.relative(package.path, from: packagesDir.parent.path); + getRelativePosixPath(package.directory, from: packagesDir.parent); if (!pubspec.repository!.path.endsWith(relativePackagePath)) { errorMessages .add('The "repository" link should end with the package path.'); diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index d09dcebce4af..ba943903b6c5 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -12,62 +12,43 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -void main() { - group('test pubspec_check_command', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - /// Returns the top section of a pubspec.yaml for a package named [name], - /// for either a flutter/packages or flutter/plugins package depending on - /// the values of [isPlugin]. - /// - /// By default it will create a header that includes all of the expected - /// values, elements can be changed via arguments to create incorrect - /// entries. - /// - /// If [includeRepository] is true, by default the path in the link will - /// be "packages/[name]"; a different "packages"-relative path can be - /// provided with [repositoryPackagesDirRelativePath]. - String headerSection( - String name, { - bool isPlugin = false, - bool includeRepository = true, - String? repositoryPackagesDirRelativePath, - bool includeHomepage = false, - bool includeIssueTracker = true, - bool publishable = true, - String? description, - }) { - final String repositoryPath = repositoryPackagesDirRelativePath ?? name; - final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/' - 'packages/$repositoryPath'; - final String issueTrackerLink = - 'https://github.com/flutter/flutter/issues?' - 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; - description ??= 'A test package for validating that the pubspec.yaml ' - 'follows repo best practices.'; - return ''' +/// Returns the top section of a pubspec.yaml for a package named [name], +/// for either a flutter/packages or flutter/plugins package depending on +/// the values of [isPlugin]. +/// +/// By default it will create a header that includes all of the expected +/// values, elements can be changed via arguments to create incorrect +/// entries. +/// +/// If [includeRepository] is true, by default the path in the link will +/// be "packages/[name]"; a different "packages"-relative path can be +/// provided with [repositoryPackagesDirRelativePath]. +String _headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + String? repositoryPackagesDirRelativePath, + bool includeHomepage = false, + bool includeIssueTracker = true, + bool publishable = true, + String? description, +}) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; + final List repoLinkPathComponents = [ + 'flutter', + if (isPlugin) 'plugins' else 'packages', + 'tree', + 'master', + 'packages', + repositoryPath, + ]; + final String repoLink = + 'https://github.com/' + repoLinkPathComponents.join('/'); + final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; + return ''' name: $name description: $description ${includeRepository ? 'repository: $repoLink' : ''} @@ -76,64 +57,89 @@ ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 ${publishable ? '' : 'publish_to: \'none\''} '''; - } +} - String environmentSection() { - return ''' +String _environmentSection() { + return ''' environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" '''; - } +} - String flutterSection({ - bool isPlugin = false, - String? implementedPackage, - }) { - final String pluginEntry = ''' +String _flutterSection({ + bool isPlugin = false, + String? implementedPackage, +}) { + final String pluginEntry = ''' plugin: ${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; - return ''' + return ''' flutter: ${isPlugin ? pluginEntry : ''} '''; - } +} - String dependenciesSection() { - return ''' +String _dependenciesSection() { + return ''' dependencies: flutter: sdk: flutter '''; - } +} - String devDependenciesSection() { - return ''' +String _devDependenciesSection() { + return ''' dev_dependencies: flutter_test: sdk: flutter '''; - } +} - String falseSecretsSection() { - return ''' +String _falseSecretsSection() { + return ''' false_secrets: - /lib/main.dart '''; - } +} + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); test('passes for a plugin following conventions', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${falseSecretsSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -154,12 +160,12 @@ ${falseSecretsSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin')} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} -${flutterSection()} -${falseSecretsSection()} +${_headerSection('plugin')} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} +${_flutterSection()} +${_falseSecretsSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -181,9 +187,9 @@ ${falseSecretsSection()} packageDirectory.createSync(recursive: true); packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('package')} -${environmentSection()} -${dependenciesSection()} +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -203,11 +209,11 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeHomepage: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -230,11 +236,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -256,11 +262,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -283,11 +289,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -309,11 +315,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -336,11 +342,11 @@ ${devDependenciesSection()} createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: 'Too short')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -365,11 +371,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: 'Too short')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -397,11 +403,11 @@ ${devDependenciesSection()} 'the core description so that search results are more useful and the ' 'package does not lose pub points.'; pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, description: description)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true, description: description)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -424,11 +430,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -451,11 +457,11 @@ ${environmentSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -478,11 +484,11 @@ ${devDependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${devDependenciesSection()} -${dependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_devDependenciesSection()} +${_dependenciesSection()} '''); Error? commandError; @@ -505,11 +511,11 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${devDependenciesSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_devDependenciesSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} '''); Error? commandError; @@ -532,12 +538,12 @@ ${dependenciesSection()} final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${falseSecretsSection()} -${devDependenciesSection()} +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_falseSecretsSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -562,11 +568,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -590,11 +596,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} -${dependenciesSection()} -${devDependenciesSection()} +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -618,15 +624,15 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a_foo', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', )} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -646,15 +652,15 @@ ${devDependenciesSection()} createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -676,16 +682,16 @@ ${devDependenciesSection()} packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin_a_platform_interface', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_platform_interface', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -707,11 +713,11 @@ ${devDependenciesSection()} // Environment section is in the wrong location. // Missing 'implements'. pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} +${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -737,17 +743,17 @@ ${environmentSection()} // Missing metadata that is only useful for published packages, such as // repository and issue tracker. pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection( +${_headerSection( 'plugin', isPlugin: true, publishable: false, includeRepository: false, includeIssueTracker: false, )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -762,4 +768,51 @@ ${devDependenciesSection()} ); }); }); + + group('test pubspec_check_command on Windows', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + test('repository check works', () async { + final Directory packageDirectory = + createFakePackage('package', packagesDir); + + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for package...'), + contains('No issues found!'), + ]), + ); + }); + }); } From 9d63517bdcf9b2be805d8ec9c4d218441c1de1e7 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Tue, 26 Oct 2021 11:27:04 -0700 Subject: [PATCH 117/259] upgraded usage of BinaryMessenger (#4451) Co-authored-by: David Iglesias Teixeira --- .cirrus.yml | 4 + ci/stable_conditional.dart | 67 +++ .../MethodCallHandlerImplTest.java | 28 +- .../plugins/camera/DartMessengerTest.java | 21 +- .../quickactions/QuickActionsTest.java | 22 +- .../MethodCallHandlerImplTest.java | 28 +- .../url_launcher_web_test.mocks.dart | 540 +++++++++--------- .../url_launcher_web/example/pubspec.yaml | 2 +- 8 files changed, 443 insertions(+), 269 deletions(-) create mode 100644 ci/stable_conditional.dart diff --git a/.cirrus.yml b/.cirrus.yml index da354c5a9b22..9776d9a2ab31 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -197,6 +197,10 @@ task: - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh lint-android # must come after build-examples + stable_channel_conditional_script: + - if [[ "$CHANNEL" == "stable" ]]; then + - dart ./ci/stable_conditional.dart + - fi native_unit_test_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. diff --git a/ci/stable_conditional.dart b/ci/stable_conditional.dart new file mode 100644 index 000000000000..76643532bc9d --- /dev/null +++ b/ci/stable_conditional.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// stable_conditional.dart +// +// Performs simple find and replace operations for conditional compilation +// before executing stable channel tests. +// +// Example input: +// int main() { +// // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE +// printf("hello world\n"); +// // FLUTTER_STABLE_CONDITIONAL_ELSE +// // printf("goodbye world\n"); +// // FLUTTER_STABLE_CONDITIONAL_ENDIF +// } +// +// Example output: +// int main() { +// printf("goodbye world\n"); +// } + +import 'dart:convert' show LineSplitter; +import 'dart:io' show FileSystemEntity, File; + +final List _filesToProcess = [ + 'packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java', + 'packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java', + 'packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java', + 'packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java', +]; + +final RegExp _replacer = RegExp( + r'^\s*// FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE(.*?)^\s*// FLUTTER_STABLE_CONDITIONAL_ELSE(.*?)^\s*// FLUTTER_STABLE_CONDITIONAL_ENDIF', + multiLine: true, + dotAll: true); +final RegExp _commentRemover = RegExp(r'^(\s*)\/\/\s*(.*)'); +const String _newline = '\n'; + +void _process(FileSystemEntity entity) { + const LineSplitter splitter = LineSplitter(); + final String text = File(entity.path).readAsStringSync(); + String replaced = ''; + int index = 0; + for (final RegExpMatch match in _replacer.allMatches(text)) { + replaced += text.substring(index, match.start); + for (final String line in splitter.convert(match.group(2)!)) { + final RegExpMatch? commentRemoverMatch = _commentRemover.firstMatch(line); + if (commentRemoverMatch != null) { + replaced += commentRemoverMatch.group(1)! + + commentRemoverMatch.group(2)! + + _newline; + } + } + index = match.end; + } + if (replaced.isNotEmpty) { + replaced += text.substring(index, text.length); + File(entity.path).writeAsStringSync(replaced); + print('modified: ${entity.path}'); + } +} + +void main(List args) { + _filesToProcess.map((String path) => File(path)).forEach(_process); +} diff --git a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java index 0ea03a0690f1..012cc9be9711 100644 --- a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -55,7 +55,11 @@ public void startListening_registersChannel() { methodCallHandler.startListening(messenger); verify(messenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -67,9 +71,15 @@ public void startListening_unregistersExistingChannel() { methodCallHandler.startListening(secondMessenger); // Unregisters the first and then registers the second. - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // verify(secondMessenger, times(1)) + // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -79,7 +89,11 @@ public void stopListening_unregistersExistingChannel() { methodCallHandler.stopListening(); - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -88,7 +102,11 @@ public void stopListening_doesNothingWhenUnset() { methodCallHandler.stopListening(); - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null, null); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 0a2fc43d03cb..1ce04d6cc104 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -12,6 +12,7 @@ import android.os.Handler; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -31,6 +32,15 @@ public class DartMessengerTest { private static class FakeBinaryMessenger implements BinaryMessenger { private final List sentMessages = new ArrayList<>(); + // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + @Override + public BinaryMessenger.TaskQueue makeBackgroundTaskQueue() { + return null; + } + // FLUTTER_STABLE_CONDITIONAL_ELSE + // FLUTTER_STABLE_CONDITIONAL_ENDIF + @Override public void send(@NonNull String channel, ByteBuffer message) { sentMessages.add(message); @@ -41,8 +51,17 @@ public void send(@NonNull String channel, ByteBuffer message, BinaryReply callba send(channel, message); } + // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE @Override - public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} + public void setMessageHandler( + @NonNull String channel, + BinaryMessageHandler handler, + @Nullable BinaryMessenger.TaskQueue taskQueue) {} + // FLUTTER_STABLE_CONDITIONAL_ELSE + // @Override + // public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} + // FLUTTER_STABLE_CONDITIONAL_ENDIF List getMessages() { return new ArrayList<>(sentMessages); diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index 208a119efafe..2b6fb495f7c4 100644 --- a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -33,6 +33,15 @@ public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { public MethodCall lastMethodCall; + // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + @Override + public BinaryMessenger.TaskQueue makeBackgroundTaskQueue() { + return null; + } + // FLUTTER_STABLE_CONDITIONAL_ELSE + // FLUTTER_STABLE_CONDITIONAL_ENDIF + @Override public void send(@NonNull String channel, @Nullable ByteBuffer message) { send(channel, message, null); @@ -49,10 +58,21 @@ public void send( } } + // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE @Override - public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { + public void setMessageHandler( + @NonNull String channel, + @Nullable BinaryMessageHandler handler, + @Nullable BinaryMessenger.TaskQueue taskQueue) { // Do nothing. } + // FLUTTER_STABLE_CONDITIONAL_ELSE + // @Override + // public void setMessageHandler( + // @NonNull String channel, + // @Nullable BinaryMessageHandler handler) {} + // FLUTTER_STABLE_CONDITIONAL_ENDIF } static final int SUPPORTED_BUILD = 25; diff --git a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java index 5e0811399ac6..6b544c7611c5 100644 --- a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java +++ b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java @@ -44,7 +44,11 @@ public void startListening_registersChannel() { methodCallHandler.startListening(messenger); verify(messenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -56,9 +60,15 @@ public void startListening_unregistersExistingChannel() { methodCallHandler.startListening(secondMessenger); // Unregisters the first and then registers the second. - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // verify(secondMessenger, times(1)) + // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -68,7 +78,11 @@ public void stopListening_unregistersExistingChannel() { methodCallHandler.stopListening(); - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test @@ -77,7 +91,11 @@ public void stopListening_doesNothingWhenUnset() { methodCallHandler.stopListening(); - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null, null); + // FLUTTER_STABLE_CONDITIONAL_ELSE + // verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + // FLUTTER_STABLE_CONDITIONAL_ENDIF } @Test diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart index 9cd0196f51db..24d694ffa0cc 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart @@ -1,57 +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. - -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.16 from annotations // in regular_integration_tests/integration_test/url_launcher_web_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; +import 'dart:async' as _i3; import 'dart:html' as _i2; -import 'dart:math' as _i5; -import 'dart:web_sql' as _i3; +import 'dart:math' as _i4; import 'package:mockito/mockito.dart' as _i1; +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types -class _FakeDocument extends _i1.Fake implements _i2.Document {} - -class _FakeLocation extends _i1.Fake implements _i2.Location {} +class _FakeDocument_0 extends _i1.Fake implements _i2.Document {} -class _FakeConsole extends _i1.Fake implements _i2.Console {} +class _FakeLocation_1 extends _i1.Fake implements _i2.Location {} -class _FakeHistory extends _i1.Fake implements _i2.History {} +class _FakeConsole_2 extends _i1.Fake implements _i2.Console {} -class _FakeStorage extends _i1.Fake implements _i2.Storage {} +class _FakeHistory_3 extends _i1.Fake implements _i2.History {} -class _FakeNavigator extends _i1.Fake implements _i2.Navigator {} +class _FakeStorage_4 extends _i1.Fake implements _i2.Storage {} -class _FakePerformance extends _i1.Fake implements _i2.Performance {} +class _FakeNavigator_5 extends _i1.Fake implements _i2.Navigator {} -class _FakeEvents extends _i1.Fake implements _i2.Events {} +class _FakePerformance_6 extends _i1.Fake implements _i2.Performance {} -class _FakeType extends _i1.Fake implements Type {} +class _FakeEvents_7 extends _i1.Fake implements _i2.Events {} -class _FakeWindowBase extends _i1.Fake implements _i2.WindowBase {} +class _FakeWindowBase_8 extends _i1.Fake implements _i2.WindowBase {} -class _FakeFileSystem extends _i1.Fake implements _i2.FileSystem {} +class _FakeFileSystem_9 extends _i1.Fake implements _i2.FileSystem {} -class _FakeStylePropertyMapReadonly extends _i1.Fake +class _FakeStylePropertyMapReadonly_10 extends _i1.Fake implements _i2.StylePropertyMapReadonly {} -class _FakeMediaQueryList extends _i1.Fake implements _i2.MediaQueryList {} - -class _FakeEntry extends _i1.Fake implements _i2.Entry {} +class _FakeMediaQueryList_11 extends _i1.Fake implements _i2.MediaQueryList {} -class _FakeSqlDatabase extends _i1.Fake implements _i3.SqlDatabase {} +class _FakeEntry_12 extends _i1.Fake implements _i2.Entry {} -class _FakeGeolocation extends _i1.Fake implements _i2.Geolocation {} +class _FakeGeolocation_13 extends _i1.Fake implements _i2.Geolocation {} -class _FakeMediaStream extends _i1.Fake implements _i2.MediaStream {} +class _FakeMediaStream_14 extends _i1.Fake implements _i2.MediaStream {} -class _FakeRelatedApplication extends _i1.Fake +class _FakeRelatedApplication_15 extends _i1.Fake implements _i2.RelatedApplication {} /// A class which mocks [Window]. @@ -63,37 +60,52 @@ class MockWindow extends _i1.Mock implements _i2.Window { } @override - _i4.Future get animationFrame => + _i3.Future get animationFrame => (super.noSuchMethod(Invocation.getter(#animationFrame), - returnValue: Future.value(0)) as _i4.Future); + returnValue: Future.value(0)) as _i3.Future); @override _i2.Document get document => (super.noSuchMethod(Invocation.getter(#document), - returnValue: _FakeDocument()) as _i2.Document); + returnValue: _FakeDocument_0()) as _i2.Document); @override _i2.Location get location => (super.noSuchMethod(Invocation.getter(#location), - returnValue: _FakeLocation()) as _i2.Location); + returnValue: _FakeLocation_1()) as _i2.Location); @override set location(_i2.LocationBase? value) => super.noSuchMethod(Invocation.setter(#location, value), returnValueForMissingStub: null); @override _i2.Console get console => (super.noSuchMethod(Invocation.getter(#console), - returnValue: _FakeConsole()) as _i2.Console); + returnValue: _FakeConsole_2()) as _i2.Console); + @override + set defaultStatus(String? value) => + super.noSuchMethod(Invocation.setter(#defaultStatus, value), + returnValueForMissingStub: null); + @override + set defaultstatus(String? value) => + super.noSuchMethod(Invocation.setter(#defaultstatus, value), + returnValueForMissingStub: null); @override num get devicePixelRatio => (super.noSuchMethod(Invocation.getter(#devicePixelRatio), returnValue: 0) as num); @override _i2.History get history => (super.noSuchMethod(Invocation.getter(#history), - returnValue: _FakeHistory()) as _i2.History); + returnValue: _FakeHistory_3()) as _i2.History); @override _i2.Storage get localStorage => (super.noSuchMethod(Invocation.getter(#localStorage), - returnValue: _FakeStorage()) as _i2.Storage); + returnValue: _FakeStorage_4()) as _i2.Storage); + @override + set name(String? value) => super.noSuchMethod(Invocation.setter(#name, value), + returnValueForMissingStub: null); @override _i2.Navigator get navigator => (super.noSuchMethod(Invocation.getter(#navigator), - returnValue: _FakeNavigator()) as _i2.Navigator); + returnValue: _FakeNavigator_5()) as _i2.Navigator); + @override + set opener(_i2.WindowBase? value) => + super.noSuchMethod(Invocation.setter(#opener, value), + returnValueForMissingStub: null); @override int get outerHeight => (super.noSuchMethod(Invocation.getter(#outerHeight), returnValue: 0) @@ -105,353 +117,357 @@ class MockWindow extends _i1.Mock implements _i2.Window { @override _i2.Performance get performance => (super.noSuchMethod(Invocation.getter(#performance), - returnValue: _FakePerformance()) as _i2.Performance); + returnValue: _FakePerformance_6()) as _i2.Performance); @override _i2.Storage get sessionStorage => (super.noSuchMethod(Invocation.getter(#sessionStorage), - returnValue: _FakeStorage()) as _i2.Storage); + returnValue: _FakeStorage_4()) as _i2.Storage); @override - _i4.Stream<_i2.Event> get onContentLoaded => + set status(String? value) => + super.noSuchMethod(Invocation.setter(#status, value), + returnValueForMissingStub: null); + @override + _i3.Stream<_i2.Event> get onContentLoaded => (super.noSuchMethod(Invocation.getter(#onContentLoaded), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onAbort => + _i3.Stream<_i2.Event> get onAbort => (super.noSuchMethod(Invocation.getter(#onAbort), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onBlur => + _i3.Stream<_i2.Event> get onBlur => (super.noSuchMethod(Invocation.getter(#onBlur), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onCanPlay => + _i3.Stream<_i2.Event> get onCanPlay => (super.noSuchMethod(Invocation.getter(#onCanPlay), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onCanPlayThrough => + _i3.Stream<_i2.Event> get onCanPlayThrough => (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onChange => + _i3.Stream<_i2.Event> get onChange => (super.noSuchMethod(Invocation.getter(#onChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.MouseEvent> get onClick => + _i3.Stream<_i2.MouseEvent> get onClick => (super.noSuchMethod(Invocation.getter(#onClick), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onContextMenu => + _i3.Stream<_i2.MouseEvent> get onContextMenu => (super.noSuchMethod(Invocation.getter(#onContextMenu), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.Event> get onDoubleClick => + _i3.Stream<_i2.Event> get onDoubleClick => (super.noSuchMethod(Invocation.getter(#onDoubleClick), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => + _i3.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => (super.noSuchMethod(Invocation.getter(#onDeviceMotion), returnValue: Stream<_i2.DeviceMotionEvent>.empty()) - as _i4.Stream<_i2.DeviceMotionEvent>); + as _i3.Stream<_i2.DeviceMotionEvent>); @override - _i4.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => + _i3.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => (super.noSuchMethod(Invocation.getter(#onDeviceOrientation), returnValue: Stream<_i2.DeviceOrientationEvent>.empty()) - as _i4.Stream<_i2.DeviceOrientationEvent>); + as _i3.Stream<_i2.DeviceOrientationEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDrag => + _i3.Stream<_i2.MouseEvent> get onDrag => (super.noSuchMethod(Invocation.getter(#onDrag), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragEnd => + _i3.Stream<_i2.MouseEvent> get onDragEnd => (super.noSuchMethod(Invocation.getter(#onDragEnd), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragEnter => + _i3.Stream<_i2.MouseEvent> get onDragEnter => (super.noSuchMethod(Invocation.getter(#onDragEnter), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragLeave => + _i3.Stream<_i2.MouseEvent> get onDragLeave => (super.noSuchMethod(Invocation.getter(#onDragLeave), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragOver => + _i3.Stream<_i2.MouseEvent> get onDragOver => (super.noSuchMethod(Invocation.getter(#onDragOver), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragStart => + _i3.Stream<_i2.MouseEvent> get onDragStart => (super.noSuchMethod(Invocation.getter(#onDragStart), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDrop => + _i3.Stream<_i2.MouseEvent> get onDrop => (super.noSuchMethod(Invocation.getter(#onDrop), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.Event> get onDurationChange => + _i3.Stream<_i2.Event> get onDurationChange => (super.noSuchMethod(Invocation.getter(#onDurationChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onEmptied => + _i3.Stream<_i2.Event> get onEmptied => (super.noSuchMethod(Invocation.getter(#onEmptied), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onEnded => + _i3.Stream<_i2.Event> get onEnded => (super.noSuchMethod(Invocation.getter(#onEnded), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onError => + _i3.Stream<_i2.Event> get onError => (super.noSuchMethod(Invocation.getter(#onError), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onFocus => + _i3.Stream<_i2.Event> get onFocus => (super.noSuchMethod(Invocation.getter(#onFocus), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onHashChange => + _i3.Stream<_i2.Event> get onHashChange => (super.noSuchMethod(Invocation.getter(#onHashChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onInput => + _i3.Stream<_i2.Event> get onInput => (super.noSuchMethod(Invocation.getter(#onInput), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onInvalid => + _i3.Stream<_i2.Event> get onInvalid => (super.noSuchMethod(Invocation.getter(#onInvalid), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyDown => + _i3.Stream<_i2.KeyboardEvent> get onKeyDown => (super.noSuchMethod(Invocation.getter(#onKeyDown), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyPress => + _i3.Stream<_i2.KeyboardEvent> get onKeyPress => (super.noSuchMethod(Invocation.getter(#onKeyPress), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyUp => + _i3.Stream<_i2.KeyboardEvent> get onKeyUp => (super.noSuchMethod(Invocation.getter(#onKeyUp), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.Event> get onLoad => + _i3.Stream<_i2.Event> get onLoad => (super.noSuchMethod(Invocation.getter(#onLoad), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadedData => + _i3.Stream<_i2.Event> get onLoadedData => (super.noSuchMethod(Invocation.getter(#onLoadedData), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadedMetadata => + _i3.Stream<_i2.Event> get onLoadedMetadata => (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadStart => + _i3.Stream<_i2.Event> get onLoadStart => (super.noSuchMethod(Invocation.getter(#onLoadStart), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.MessageEvent> get onMessage => + _i3.Stream<_i2.MessageEvent> get onMessage => (super.noSuchMethod(Invocation.getter(#onMessage), returnValue: Stream<_i2.MessageEvent>.empty()) - as _i4.Stream<_i2.MessageEvent>); + as _i3.Stream<_i2.MessageEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseDown => + _i3.Stream<_i2.MouseEvent> get onMouseDown => (super.noSuchMethod(Invocation.getter(#onMouseDown), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseEnter => + _i3.Stream<_i2.MouseEvent> get onMouseEnter => (super.noSuchMethod(Invocation.getter(#onMouseEnter), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseLeave => + _i3.Stream<_i2.MouseEvent> get onMouseLeave => (super.noSuchMethod(Invocation.getter(#onMouseLeave), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseMove => + _i3.Stream<_i2.MouseEvent> get onMouseMove => (super.noSuchMethod(Invocation.getter(#onMouseMove), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseOut => + _i3.Stream<_i2.MouseEvent> get onMouseOut => (super.noSuchMethod(Invocation.getter(#onMouseOut), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseOver => + _i3.Stream<_i2.MouseEvent> get onMouseOver => (super.noSuchMethod(Invocation.getter(#onMouseOver), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseUp => + _i3.Stream<_i2.MouseEvent> get onMouseUp => (super.noSuchMethod(Invocation.getter(#onMouseUp), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.WheelEvent> get onMouseWheel => + _i3.Stream<_i2.WheelEvent> get onMouseWheel => (super.noSuchMethod(Invocation.getter(#onMouseWheel), returnValue: Stream<_i2.WheelEvent>.empty()) - as _i4.Stream<_i2.WheelEvent>); + as _i3.Stream<_i2.WheelEvent>); @override - _i4.Stream<_i2.Event> get onOffline => + _i3.Stream<_i2.Event> get onOffline => (super.noSuchMethod(Invocation.getter(#onOffline), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onOnline => + _i3.Stream<_i2.Event> get onOnline => (super.noSuchMethod(Invocation.getter(#onOnline), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPageHide => + _i3.Stream<_i2.Event> get onPageHide => (super.noSuchMethod(Invocation.getter(#onPageHide), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPageShow => + _i3.Stream<_i2.Event> get onPageShow => (super.noSuchMethod(Invocation.getter(#onPageShow), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPause => + _i3.Stream<_i2.Event> get onPause => (super.noSuchMethod(Invocation.getter(#onPause), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPlay => + _i3.Stream<_i2.Event> get onPlay => (super.noSuchMethod(Invocation.getter(#onPlay), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPlaying => + _i3.Stream<_i2.Event> get onPlaying => (super.noSuchMethod(Invocation.getter(#onPlaying), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.PopStateEvent> get onPopState => + _i3.Stream<_i2.PopStateEvent> get onPopState => (super.noSuchMethod(Invocation.getter(#onPopState), returnValue: Stream<_i2.PopStateEvent>.empty()) - as _i4.Stream<_i2.PopStateEvent>); + as _i3.Stream<_i2.PopStateEvent>); @override - _i4.Stream<_i2.Event> get onProgress => + _i3.Stream<_i2.Event> get onProgress => (super.noSuchMethod(Invocation.getter(#onProgress), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onRateChange => + _i3.Stream<_i2.Event> get onRateChange => (super.noSuchMethod(Invocation.getter(#onRateChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onReset => + _i3.Stream<_i2.Event> get onReset => (super.noSuchMethod(Invocation.getter(#onReset), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onResize => + _i3.Stream<_i2.Event> get onResize => (super.noSuchMethod(Invocation.getter(#onResize), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onScroll => + _i3.Stream<_i2.Event> get onScroll => (super.noSuchMethod(Invocation.getter(#onScroll), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSearch => + _i3.Stream<_i2.Event> get onSearch => (super.noSuchMethod(Invocation.getter(#onSearch), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSeeked => + _i3.Stream<_i2.Event> get onSeeked => (super.noSuchMethod(Invocation.getter(#onSeeked), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSeeking => + _i3.Stream<_i2.Event> get onSeeking => (super.noSuchMethod(Invocation.getter(#onSeeking), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSelect => + _i3.Stream<_i2.Event> get onSelect => (super.noSuchMethod(Invocation.getter(#onSelect), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onStalled => + _i3.Stream<_i2.Event> get onStalled => (super.noSuchMethod(Invocation.getter(#onStalled), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.StorageEvent> get onStorage => + _i3.Stream<_i2.StorageEvent> get onStorage => (super.noSuchMethod(Invocation.getter(#onStorage), returnValue: Stream<_i2.StorageEvent>.empty()) - as _i4.Stream<_i2.StorageEvent>); + as _i3.Stream<_i2.StorageEvent>); @override - _i4.Stream<_i2.Event> get onSubmit => + _i3.Stream<_i2.Event> get onSubmit => (super.noSuchMethod(Invocation.getter(#onSubmit), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSuspend => + _i3.Stream<_i2.Event> get onSuspend => (super.noSuchMethod(Invocation.getter(#onSuspend), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onTimeUpdate => + _i3.Stream<_i2.Event> get onTimeUpdate => (super.noSuchMethod(Invocation.getter(#onTimeUpdate), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.TouchEvent> get onTouchCancel => + _i3.Stream<_i2.TouchEvent> get onTouchCancel => (super.noSuchMethod(Invocation.getter(#onTouchCancel), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchEnd => + _i3.Stream<_i2.TouchEvent> get onTouchEnd => (super.noSuchMethod(Invocation.getter(#onTouchEnd), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchMove => + _i3.Stream<_i2.TouchEvent> get onTouchMove => (super.noSuchMethod(Invocation.getter(#onTouchMove), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchStart => + _i3.Stream<_i2.TouchEvent> get onTouchStart => (super.noSuchMethod(Invocation.getter(#onTouchStart), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TransitionEvent> get onTransitionEnd => + _i3.Stream<_i2.TransitionEvent> get onTransitionEnd => (super.noSuchMethod(Invocation.getter(#onTransitionEnd), returnValue: Stream<_i2.TransitionEvent>.empty()) - as _i4.Stream<_i2.TransitionEvent>); + as _i3.Stream<_i2.TransitionEvent>); @override - _i4.Stream<_i2.Event> get onUnload => + _i3.Stream<_i2.Event> get onUnload => (super.noSuchMethod(Invocation.getter(#onUnload), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onVolumeChange => + _i3.Stream<_i2.Event> get onVolumeChange => (super.noSuchMethod(Invocation.getter(#onVolumeChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onWaiting => + _i3.Stream<_i2.Event> get onWaiting => (super.noSuchMethod(Invocation.getter(#onWaiting), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationEnd => + _i3.Stream<_i2.AnimationEvent> get onAnimationEnd => (super.noSuchMethod(Invocation.getter(#onAnimationEnd), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationIteration => + _i3.Stream<_i2.AnimationEvent> get onAnimationIteration => (super.noSuchMethod(Invocation.getter(#onAnimationIteration), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationStart => + _i3.Stream<_i2.AnimationEvent> get onAnimationStart => (super.noSuchMethod(Invocation.getter(#onAnimationStart), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.Event> get onBeforeUnload => + _i3.Stream<_i2.Event> get onBeforeUnload => (super.noSuchMethod(Invocation.getter(#onBeforeUnload), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.WheelEvent> get onWheel => + _i3.Stream<_i2.WheelEvent> get onWheel => (super.noSuchMethod(Invocation.getter(#onWheel), returnValue: Stream<_i2.WheelEvent>.empty()) - as _i4.Stream<_i2.WheelEvent>); + as _i3.Stream<_i2.WheelEvent>); @override int get pageXOffset => (super.noSuchMethod(Invocation.getter(#pageXOffset), returnValue: 0) @@ -468,18 +484,12 @@ class MockWindow extends _i1.Mock implements _i2.Window { (super.noSuchMethod(Invocation.getter(#scrollY), returnValue: 0) as int); @override _i2.Events get on => - (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents()) + (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents_7()) as _i2.Events); @override - int get hashCode => - (super.noSuchMethod(Invocation.getter(#hashCode), returnValue: 0) as int); - @override - Type get runtimeType => (super.noSuchMethod(Invocation.getter(#runtimeType), - returnValue: _FakeType()) as Type); - @override _i2.WindowBase open(String? url, String? name, [String? options]) => (super.noSuchMethod(Invocation.method(#open, [url, name, options]), - returnValue: _FakeWindowBase()) as _i2.WindowBase); + returnValue: _FakeWindowBase_8()) as _i2.WindowBase); @override int requestAnimationFrame(_i2.FrameRequestCallback? callback) => (super.noSuchMethod(Invocation.method(#requestAnimationFrame, [callback]), @@ -489,25 +499,32 @@ class MockWindow extends _i1.Mock implements _i2.Window { super.noSuchMethod(Invocation.method(#cancelAnimationFrame, [id]), returnValueForMissingStub: null); @override - _i4.Future<_i2.FileSystem> requestFileSystem(int? size, + _i3.Future<_i2.FileSystem> requestFileSystem(int? size, {bool? persistent = false}) => (super.noSuchMethod( Invocation.method( #requestFileSystem, [size], {#persistent: persistent}), - returnValue: Future.value(_FakeFileSystem())) - as _i4.Future<_i2.FileSystem>); + returnValue: Future<_i2.FileSystem>.value(_FakeFileSystem_9())) + as _i3.Future<_i2.FileSystem>); + @override + void alert([String? message]) => + super.noSuchMethod(Invocation.method(#alert, [message]), + returnValueForMissingStub: null); @override void cancelIdleCallback(int? handle) => super.noSuchMethod(Invocation.method(#cancelIdleCallback, [handle]), returnValueForMissingStub: null); @override + void close() => super.noSuchMethod(Invocation.method(#close, []), + returnValueForMissingStub: null); + @override bool confirm([String? message]) => (super.noSuchMethod(Invocation.method(#confirm, [message]), returnValue: false) as bool); @override - _i4.Future fetch(dynamic input, [Map? init]) => + _i3.Future fetch(dynamic input, [Map? init]) => (super.noSuchMethod(Invocation.method(#fetch, [input, init]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override bool find(String? string, bool? caseSensitive, bool? backwards, bool? wrap, bool? wholeWord, bool? searchInFrames, bool? showDialog) => @@ -527,7 +544,7 @@ class MockWindow extends _i1.Mock implements _i2.Window { _i2.Element? element, String? pseudoElement) => (super.noSuchMethod( Invocation.method(#getComputedStyleMap, [element, pseudoElement]), - returnValue: _FakeStylePropertyMapReadonly()) + returnValue: _FakeStylePropertyMapReadonly_10()) as _i2.StylePropertyMapReadonly); @override List<_i2.CssRule> getMatchedCssRules( @@ -538,7 +555,7 @@ class MockWindow extends _i1.Mock implements _i2.Window { @override _i2.MediaQueryList matchMedia(String? query) => (super.noSuchMethod(Invocation.method(#matchMedia, [query]), - returnValue: _FakeMediaQueryList()) as _i2.MediaQueryList); + returnValue: _FakeMediaQueryList_11()) as _i2.MediaQueryList); @override void moveBy(int? x, int? y) => super.noSuchMethod(Invocation.method(#moveBy, [x, y]), @@ -550,6 +567,9 @@ class MockWindow extends _i1.Mock implements _i2.Window { Invocation.method(#postMessage, [message, targetOrigin, transfer]), returnValueForMissingStub: null); @override + void print() => super.noSuchMethod(Invocation.method(#print, []), + returnValueForMissingStub: null); + @override int requestIdleCallback(_i2.IdleRequestCallback? callback, [Map? options]) => (super.noSuchMethod( @@ -564,9 +584,37 @@ class MockWindow extends _i1.Mock implements _i2.Window { super.noSuchMethod(Invocation.method(#resizeTo, [x, y]), returnValueForMissingStub: null); @override - _i4.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => + void scroll( + [dynamic options_OR_x, + dynamic y, + Map? scrollOptions]) => + super.noSuchMethod( + Invocation.method(#scroll, [options_OR_x, y, scrollOptions]), + returnValueForMissingStub: null); + @override + void scrollBy( + [dynamic options_OR_x, + dynamic y, + Map? scrollOptions]) => + super.noSuchMethod( + Invocation.method(#scrollBy, [options_OR_x, y, scrollOptions]), + returnValueForMissingStub: null); + @override + void scrollTo( + [dynamic options_OR_x, + dynamic y, + Map? scrollOptions]) => + super.noSuchMethod( + Invocation.method(#scrollTo, [options_OR_x, y, scrollOptions]), + returnValueForMissingStub: null); + @override + void stop() => super.noSuchMethod(Invocation.method(#stop, []), + returnValueForMissingStub: null); + @override + _i3.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => (super.noSuchMethod(Invocation.method(#resolveLocalFileSystemUrl, [url]), - returnValue: Future.value(_FakeEntry())) as _i4.Future<_i2.Entry>); + returnValue: Future<_i2.Entry>.value(_FakeEntry_12())) + as _i3.Future<_i2.Entry>); @override String atob(String? atob) => (super.noSuchMethod(Invocation.method(#atob, [atob]), returnValue: '') @@ -576,18 +624,10 @@ class MockWindow extends _i1.Mock implements _i2.Window { (super.noSuchMethod(Invocation.method(#btoa, [btoa]), returnValue: '') as String); @override - void moveTo(_i5.Point? p) => + void moveTo(_i4.Point? p) => super.noSuchMethod(Invocation.method(#moveTo, [p]), returnValueForMissingStub: null); @override - _i3.SqlDatabase openDatabase(String? name, String? version, - String? displayName, int? estimatedSize, - [_i2.DatabaseCallback? creationCallback]) => - (super.noSuchMethod( - Invocation.method(#openDatabase, - [name, version, displayName, estimatedSize, creationCallback]), - returnValue: _FakeSqlDatabase()) as _i3.SqlDatabase); - @override void addEventListener(String? type, _i2.EventListener? listener, [bool? useCapture]) => super.noSuchMethod( @@ -604,13 +644,7 @@ class MockWindow extends _i1.Mock implements _i2.Window { (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), returnValue: false) as bool); @override - bool operator ==(Object? other) => - (super.noSuchMethod(Invocation.method(#==, [other]), returnValue: false) - as bool); - @override - String toString() => - (super.noSuchMethod(Invocation.method(#toString, []), returnValue: '') - as String); + String toString() => super.toString(); } /// A class which mocks [Navigator]. @@ -628,7 +662,7 @@ class MockNavigator extends _i1.Mock implements _i2.Navigator { @override _i2.Geolocation get geolocation => (super.noSuchMethod(Invocation.getter(#geolocation), - returnValue: _FakeGeolocation()) as _i2.Geolocation); + returnValue: _FakeGeolocation_13()) as _i2.Geolocation); @override String get vendor => (super.noSuchMethod(Invocation.getter(#vendor), returnValue: '') @@ -658,69 +692,63 @@ class MockNavigator extends _i1.Mock implements _i2.Navigator { (super.noSuchMethod(Invocation.getter(#userAgent), returnValue: '') as String); @override - int get hashCode => - (super.noSuchMethod(Invocation.getter(#hashCode), returnValue: 0) as int); - @override - Type get runtimeType => (super.noSuchMethod(Invocation.getter(#runtimeType), - returnValue: _FakeType()) as Type); - @override List<_i2.Gamepad?> getGamepads() => (super.noSuchMethod(Invocation.method(#getGamepads, []), returnValue: <_i2.Gamepad?>[]) as List<_i2.Gamepad?>); @override - _i4.Future<_i2.MediaStream> getUserMedia( + _i3.Future<_i2.MediaStream> getUserMedia( {dynamic audio = false, dynamic video = false}) => (super.noSuchMethod( Invocation.method(#getUserMedia, [], {#audio: audio, #video: video}), returnValue: - Future.value(_FakeMediaStream())) as _i4.Future<_i2.MediaStream>); + Future<_i2.MediaStream>.value(_FakeMediaStream_14())) as _i3 + .Future<_i2.MediaStream>); @override - _i4.Future getBattery() => + void cancelKeyboardLock() => + super.noSuchMethod(Invocation.method(#cancelKeyboardLock, []), + returnValueForMissingStub: null); + @override + _i3.Future getBattery() => (super.noSuchMethod(Invocation.method(#getBattery, []), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future<_i2.RelatedApplication> getInstalledRelatedApps() => + _i3.Future<_i2.RelatedApplication> getInstalledRelatedApps() => (super.noSuchMethod(Invocation.method(#getInstalledRelatedApps, []), - returnValue: Future.value(_FakeRelatedApplication())) - as _i4.Future<_i2.RelatedApplication>); + returnValue: Future<_i2.RelatedApplication>.value( + _FakeRelatedApplication_15())) + as _i3.Future<_i2.RelatedApplication>); @override - _i4.Future getVRDisplays() => + _i3.Future getVRDisplays() => (super.noSuchMethod(Invocation.method(#getVRDisplays, []), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override void registerProtocolHandler(String? scheme, String? url, String? title) => super.noSuchMethod( Invocation.method(#registerProtocolHandler, [scheme, url, title]), returnValueForMissingStub: null); @override - _i4.Future requestKeyboardLock([List? keyCodes]) => + _i3.Future requestKeyboardLock([List? keyCodes]) => (super.noSuchMethod(Invocation.method(#requestKeyboardLock, [keyCodes]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future requestMidiAccess([Map? options]) => + _i3.Future requestMidiAccess([Map? options]) => (super.noSuchMethod(Invocation.method(#requestMidiAccess, [options]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future requestMediaKeySystemAccess(String? keySystem, + _i3.Future requestMediaKeySystemAccess(String? keySystem, List>? supportedConfigurations) => (super.noSuchMethod( Invocation.method(#requestMediaKeySystemAccess, [keySystem, supportedConfigurations]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override bool sendBeacon(String? url, Object? data) => (super.noSuchMethod(Invocation.method(#sendBeacon, [url, data]), returnValue: false) as bool); @override - _i4.Future share([Map? data]) => + _i3.Future share([Map? data]) => (super.noSuchMethod(Invocation.method(#share, [data]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - bool operator ==(Object? other) => - (super.noSuchMethod(Invocation.method(#==, [other]), returnValue: false) - as bool); - @override - String toString() => - (super.noSuchMethod(Invocation.method(#toString, []), returnValue: '') - as String); + String toString() => super.toString(); } diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index 7c00c33550a8..872bc8873b5c 100644 --- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^1.10.0 + build_runner: ^2.1.1 mockito: ^5.0.0 url_launcher_web: path: ../ From 97c2483be0d67d2a89c5f2d9792c8d3f41c9b917 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 26 Oct 2021 13:52:31 -0700 Subject: [PATCH 118/259] Implement Android WebView api with pigeon (Dart portion) (#4435) --- .../generatePigeons.sh | 8 + .../lib/src/android_webview.dart | 648 +++++++ .../lib/src/android_webview.pigeon.dart | 1601 +++++++++++++++++ .../lib/src/android_webview_api_impls.dart | 622 +++++++ .../lib/src/instance_manager.dart | 52 + .../pigeons/android_webview.dart | 181 ++ .../webview_flutter_android/pubspec.yaml | 1 + .../test/android_webview.pigeon.dart | 1000 ++++++++++ .../test/android_webview_test.dart | 373 ++++ .../test/instance_manager_test.dart | 35 + 10 files changed, 4521 insertions(+) create mode 100755 packages/webview_flutter/webview_flutter_android/generatePigeons.sh create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart create mode 100644 packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart diff --git a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh new file mode 100755 index 000000000000..d866473636cc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh @@ -0,0 +1,8 @@ +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +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 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 new file mode 100644 index 000000000000..1ddd6f3d9f0f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -0,0 +1,648 @@ +// Copyright 2013 The Flutter Authors. All 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/widgets.dart' show AndroidViewSurface; + +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** +/// In most cases, we recommend using a standard web browser, like Chrome, to +/// deliver content to the user. To learn more about web browsers, read the +/// guide on invoking a browser with +/// [url_launcher](https://pub.dev/packages/url_launcher). +/// +/// WebView objects allow you to display web content as part of your widget +/// layout, but lack some of the features of fully-developed browsers. A WebView +/// is useful when you need increased control over the UI and advanced +/// configuration options that will allow you to embed web pages in a +/// specially-designed environment for your app. +/// +/// To learn more about WebView and alternatives for serving web content, read +/// the documentation on +/// [Web-based content](https://developer.android.com/guide/webapps). +class WebView { + /// Constructs a new WebView. + WebView({this.useHybridComposition = false}) { + api.createFromInstance(this); + } + + /// Pigeon Host Api implementation for [WebView]. + @visibleForTesting + static WebViewHostApiImpl api = WebViewHostApiImpl(); + + WebViewClient? _currentWebViewClient; + DownloadListener? _currentDownloadListener; + WebChromeClient? _currentWebChromeClient; + Set _javaScriptChannels = {}; + + /// Whether the [WebView] will be rendered with an [AndroidViewSurface]. + /// + /// This implementation uses hybrid composition to render the WebView Widget. + /// This comes at the cost of some performance on Android versions below 10. + /// See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance + /// for more information. + /// + /// Defaults to false. + final bool useHybridComposition; + + /// The [WebSettings] object used to control the settings for this WebView. + late final WebSettings settings = WebSettings(this); + + /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. + /// + /// This flag can be enabled in order to facilitate debugging of web layouts + /// and JavaScript code running inside WebViews. Please refer to [WebView] + /// documentation for the debugging guide. The default is false. + static Future setWebContentsDebuggingEnabled(bool enabled) { + return api.setWebContentsDebuggingEnabled(enabled); + } + + /// Loads the given URL with additional HTTP headers, specified as a map from name to value. + /// + /// Note that if this map contains any of the headers that are set by default + /// by this WebView, such as those controlling caching, accept types or the + /// User-Agent, their values may be overridden by this WebView's defaults. + /// + /// Also see compatibility note on [evaluateJavascript]. + Future loadUrl(String url, Map headers) { + return api.loadUrlFromInstance(this, url, headers); + } + + /// Gets the URL for the current page. + /// + /// This is not always the same as the URL passed to + /// [WebViewClient.onPageStarted] because although the load for that URL has + /// 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; + } + + /// Whether this WebView has a back history item. + Future canGoBack() { + return api.canGoBackFromInstance(this); + } + + /// Whether this WebView has a forward history item. + Future canGoForward() { + return api.canGoForwardFromInstance(this); + } + + /// Goes back in the history of this WebView. + Future goBack() { + return api.goBackFromInstance(this); + } + + /// Goes forward in the history of this WebView. + Future goForward() { + return api.goForwardFromInstance(this); + } + + /// Reloads the current URL. + Future reload() { + return api.reloadFromInstance(this); + } + + /// Clears the resource cache. + /// + /// Note that the cache is per-application, so this will clear the cache for + /// all WebViews used. + Future clearCache(bool includeDiskFiles) { + return api.clearCacheFromInstance(this, includeDiskFiles); + } + + // TODO(bparrishMines): Update documentation once addJavascriptInterface is added. + /// Asynchronously evaluates JavaScript in the context of the currently displayed page. + /// + /// If non-null, the returned value will be any result returned from that + /// execution. + /// + /// Compatibility note. Applications targeting Android versions N or later, + /// 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( + 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; + } + + // TODO(bparrishMines): Update documentation when onScrollChanged is added. + /// Set the scrolled position of your view. + Future scrollTo(int x, int y) { + return api.scrollToFromInstance(this, x, y); + } + + // TODO(bparrishMines): Update documentation when onScrollChanged is added. + /// Move the scrolled position of your view. + Future scrollBy(int x, int y) { + return api.scrollByFromInstance(this, x, y); + } + + /// Return the scrolled left position of this view. + /// + /// This is the left edge of the displayed part of your view. You do not + /// need to draw any pixels farther left, since those are outside of the frame + /// of your view on screen. + Future getScrollX() { + return api.getScrollXFromInstance(this); + } + + /// Return the scrolled top position of this view. + /// + /// This is the top edge of the displayed part of your view. You do not need + /// to draw any pixels above it, since those are outside of the frame of your + /// view on screen. + Future getScrollY() { + return api.getScrollYFromInstance(this); + } + + /// Sets the [WebViewClient] that will receive various notifications and requests. + /// + /// This will replace the current handler. + Future setWebViewClient(WebViewClient webViewClient) { + final WebViewClient? currentWebViewClient = _currentWebViewClient; + + if (webViewClient == currentWebViewClient) { + return Future.value(); + } + + if (currentWebViewClient != null) { + WebViewClient.api.disposeFromInstance(currentWebViewClient); + } + + WebViewClient.api.createFromInstance(webViewClient); + _currentWebViewClient = webViewClient; + return api.setWebViewClientFromInstance(this, webViewClient); + } + + /// Injects the supplied [JavascriptChannel] into this WebView. + /// + /// The object is injected into all frames of the web page, including all the + /// iframes, using the supplied name. This allows the object's methods to + /// be accessed from JavaScript. + /// + /// Note that injected objects will not appear in JavaScript until the page is + /// next (re)loaded. JavaScript should be enabled before injecting the object. + /// For example: + /// + /// ```dart + /// webview.settings.setJavaScriptEnabled(true); + /// webView.addJavascriptChannel(JavScriptChannel("injectedObject")); + /// webView.loadUrl("about:blank", {}); + /// webView.loadUrl("javascript:injectedObject.postMessage("Hello, World!")", {}); + /// ``` + /// + /// **Important** + /// * Because the object is exposed to all the frames, any frame could obtain + /// the object name and call methods on it. There is no way to tell the + /// calling frame's origin from the app side, so the app must not assume that + /// the caller is trustworthy unless the app can guarantee that no third party + /// content is ever loaded into the WebView even inside an iframe. + Future addJavaScriptChannel(JavaScriptChannel javaScriptChannel) { + JavaScriptChannel.api.createFromInstance(javaScriptChannel); + _javaScriptChannels.add(javaScriptChannel); + return api.addJavaScriptChannelFromInstance(this, javaScriptChannel); + } + + /// Removes a previously injected [JavaScriptChannel] from this WebView. + /// + /// Note that the removal will not be reflected in JavaScript until the page + /// is next (re)loaded. See [addJavaScriptChannel]. + Future removeJavaScriptChannel(JavaScriptChannel javaScriptChannel) { + _javaScriptChannels.remove(javaScriptChannel); + api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); + return JavaScriptChannel.api.disposeFromInstance(javaScriptChannel); + } + + /// 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) { + final DownloadListener? currentDownloadListener = _currentDownloadListener; + + if (listener == currentDownloadListener) { + return Future.value(); + } + + if (currentDownloadListener != null) { + DownloadListener.api.disposeFromInstance(currentDownloadListener); + } + + DownloadListener.api.createFromInstance(listener); + _currentDownloadListener = listener; + return api.setDownloadListenerFromInstance(this, listener); + } + + /// Sets the chrome handler. + /// + /// This is an implementation of [WebChromeClient] for use in handling + /// JavaScript dialogs, favicons, titles, and the progress. This will replace + /// the current handler. + Future setWebChromeClient(WebChromeClient client) { + final WebChromeClient? currentWebChromeClient = _currentWebChromeClient; + + if (client == currentWebChromeClient) { + return Future.value(); + } + + if (currentWebChromeClient != null) { + WebChromeClient.api.disposeFromInstance(currentWebChromeClient); + } + + final WebViewClient? currentWebViewClient = _currentWebViewClient; + assert( + currentWebViewClient != null, + "Can't set a WebChromeClient without setting a WebViewClient first.", + ); + + WebChromeClient.api.createFromInstance(client, currentWebViewClient!); + _currentWebChromeClient = client; + return api.setWebChromeClientFromInstance(this, client); + } +} + +/// Manages settings state for a [WebView]. +/// +/// When a WebView is first created, it obtains a set of default settings. These +/// default settings will be returned from any getter call. A WebSettings object +/// obtained from [WebView.settings] is tied to the life of the WebView. If a +/// WebView has been destroyed, any method call on [WebSettings] will throw an +/// Exception. +class WebSettings { + /// Constructs a [WebSettings]. + /// + /// This constructor is only used for testing. An instance should be obtained + /// with [WebView.settings]. + @visibleForTesting + WebSettings(WebView webView) { + api.createFromInstance(this, webView); + } + + /// Pigeon Host Api implementation for [WebSettings]. + @visibleForTesting + static WebSettingsHostApiImpl api = WebSettingsHostApiImpl(); + + /// Sets whether the DOM storage API is enabled. + /// + /// The default value is false. + Future setDomStorageEnabled(bool flag) { + return api.setDomStorageEnabledFromInstance(this, flag); + } + + /// Tells JavaScript to open windows automatically. + /// + /// This applies to the JavaScript function `window.open()`. The default is + /// false. + Future setJavaScriptCanOpenWindowsAutomatically(bool flag) { + return api.setJavaScriptCanOpenWindowsAutomaticallyFromInstance( + this, + flag, + ); + } + + // TODO(bparrishMines): Update documentation when WebChromeClient.onCreateWindow is added. + /// Sets whether the WebView should supports multiple windows. + /// + /// The default is false. + Future setSupportMultipleWindows(bool support) { + return api.setSupportZoomFromInstance(this, support); + } + + /// Tells the WebView to enable JavaScript execution. + /// + /// The default is false. + Future setJavaScriptEnabled(bool flag) { + return api.setJavaScriptEnabledFromInstance(this, flag); + } + + /// Sets the WebView's user-agent string. + /// + /// 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) { + return api.setUserAgentStringFromInstance(this, userAgentString); + } + + /// Sets whether the WebView requires a user gesture to play media. + /// + /// The default is true. + Future setMediaPlaybackRequiresUserGesture(bool require) { + return api.setMediaPlaybackRequiresUserGestureFromInstance(this, require); + } + + // TODO(bparrishMines): Update documentation when WebView.zoomIn and WebView.zoomOut are added. + /// Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// The particular zoom mechanisms that should be used can be set with + /// [setBuiltInZoomControls]. + /// + /// The default is true. + Future setSupportZoom(bool support) { + return api.setSupportZoomFromInstance(this, support); + } + + /// Sets whether the WebView loads pages in overview mode, that is, zooms out the content to fit on screen by width. + /// + /// This setting is taken into account when the content width is greater than + /// the width of the WebView control, for example, when [setUseWideViewPort] + /// is enabled. + /// + /// The default is false. + Future setLoadWithOverviewMode(bool overview) { + return api.setLoadWithOverviewModeFromInstance(this, overview); + } + + /// Sets whether the WebView should enable support for the "viewport" HTML meta tag or should use a wide viewport. + /// + /// When the value of the setting is false, the layout width is always set to + /// the width of the WebView control in device-independent (CSS) pixels. When + /// the value is true and the page contains the viewport meta tag, the value + /// of the width specified in the tag is used. If the page does not contain + /// the tag or does not provide a width, then a wide viewport will be used. + Future setUseWideViewPort(bool use) { + return api.setUseWideViewPortFromInstance(this, use); + } + + // TODO(bparrishMines): Update documentation when ZoomButtonsController is added. + /// Sets whether the WebView should display on-screen zoom controls when using the built-in zoom mechanisms. + /// + /// See [setBuiltInZoomControls]. The default is true. However, on-screen zoom + /// controls are deprecated in Android so it's recommended to set this to + /// false. + Future setDisplayZoomControls(bool enabled) { + return api.setDisplayZoomControlsFromInstance(this, enabled); + } + + // TODO(bparrishMines): Update documentation when ZoomButtonsController is added. + /// Sets whether the WebView should use its built-in zoom mechanisms. + /// + /// The built-in zoom mechanisms comprise on-screen zoom controls, which are + /// displayed over the WebView's content, and the use of a pinch gesture to + /// control zooming. Whether or not these on-screen controls are displayed can + /// be set with [setDisplayZoomControls]. The default is false. + /// + /// The built-in mechanisms are the only currently supported zoom mechanisms, + /// so it is recommended that this setting is always enabled. However, + /// on-screen zoom controls are deprecated in Android so it's recommended to + /// disable [setDisplayZoomControls]. + Future setBuiltInZoomControls(bool enabled) { + return api.setBuiltInZoomControlsFromInstance(this, enabled); + } +} + +/// Exposes a channel to receive calls from javaScript. +/// +/// See [WebView.addJavaScriptChannel]. +abstract class JavaScriptChannel { + /// Constructs a [JavaScriptChannel]. + JavaScriptChannel(this.channelName); + + /// Pigeon Host Api implementation for [JavaScriptChannel]. + @visibleForTesting + static JavaScriptChannelHostApiImpl api = JavaScriptChannelHostApiImpl(); + + /// Used to identify this object to receive messages from javaScript. + final String channelName; + + /// Callback method when javaScript calls `postMessage` on the object instance passed. + void postMessage(String message) {} +} + +/// Receive various notifications and requests for [WebView]. +abstract class WebViewClient { + /// Constructs a [WebViewClient]. + WebViewClient({this.shouldOverrideUrlLoading = true}); + + /// User authentication failed on server. + static const int errorAuthentication = 0xfffffffc; + + /// Malformed URL. + static const int errorBadUrl = 0xfffffff4; + + /// Failed to connect to the server. + static const int errorConnect = 0xfffffffa; + + /// Failed to perform SSL handshake. + static const int errorFailedSslHandshake = 0xfffffff5; + + /// Generic file error. + static const int errorFile = 0xfffffff3; + + /// File not found. + static const int errorFileNotFound = 0xfffffff2; + + /// Server or proxy hostname lookup failed. + static const int errorHostLookup = 0xfffffffe; + + /// Failed to read or write to the server. + static const int errorIO = 0xfffffff9; + + /// User authentication failed on proxy. + static const int errorProxyAuthentication = 0xfffffffb; + + /// Too many redirects. + static const int errorRedirectLoop = 0xfffffff7; + + /// Connection timed out. + static const int errorTimeout = 0xfffffff8; + + /// Too many requests during this load. + static const int errorTooManyRequests = 0xfffffff1; + + /// Generic error. + static const int errorUnknown = 0xffffffff; + + /// Resource load was canceled by Safe Browsing. + static const int errorUnsafeResource = 0xfffffff0; + + /// Unsupported authentication scheme (not basic or digest). + static const int errorUnsupportedAuthScheme = 0xfffffffd; + + /// Unsupported URI scheme. + static const int errorUnsupportedScheme = 0xfffffff6; + + /// Pigeon Host Api implementation for [WebViewClient]. + @visibleForTesting + static WebViewClientHostApiImpl api = WebViewClientHostApiImpl(); + + /// Whether loading a url should be overridden. + /// + /// In Java, `shouldOverrideUrlLoading()` and `shouldOverrideRequestLoading()` + /// callbacks must synchronously return a boolean. This sets the default + /// return value. + /// + /// Setting [shouldOverrideUrlLoading] to true causes the current [WebView] to + /// abort loading the URL, while returning false causes the [WebView] to + /// continue loading the URL as usual. [requestLoading] or [urlLoading] will + /// still be called either way. + /// + /// Defaults to true. + final bool shouldOverrideUrlLoading; + + /// Notify the host application that a page has started loading. + /// + /// This method is called once for each main frame load so a page with iframes + /// or framesets will call onPageStarted one time for the main frame. This + /// also means that [onPageStarted] will not be called when the contents of an + /// embedded frame changes, i.e. clicking a link whose target is an iframe, it + /// will also not be called for fragment navigations (navigations to + /// #fragment_id). + void onPageStarted(WebView webView, String url) {} + + // TODO(bparrishMines): Update documentation when WebView.postVisualStateCallback is added. + /// Notify the host application that a page has finished loading. + /// + /// This method is called only for main frame. Receiving an [onPageFinished] + /// callback does not guarantee that the next frame drawn by WebView will + /// reflect the state of the DOM at this point. + void onPageFinished(WebView webView, String url) {} + + /// Report web resource loading error to the host application. + /// + /// These errors usually indicate inability to connect to the server. Note + /// that unlike the deprecated version of the callback, the new version will + /// be called for any resource (iframe, image, etc.), not just for the main + /// page. Thus, it is recommended to perform minimum required work in this + /// callback. + void onReceivedRequestError( + WebView webView, + WebResourceRequest request, + WebResourceError error, + ) {} + + /// Report an error to the host application. + /// + /// These errors are unrecoverable (i.e. the main resource is unavailable). + /// The errorCode parameter corresponds to one of the error* constants. + @Deprecated('Only called on Android version < 23.') + void onReceivedError( + WebView webView, + int errorCode, + String description, + String failingUrl, + ) {} + + // TODO(bparrishMines): Update documentation once synchronous url handling is supported. + /// When a URL is about to be loaded in the current [WebView]. + /// + /// If a [WebViewClient] is not provided, by default [WebView] will ask + /// Activity Manager to choose the proper handler for the URL. If a + /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true + /// causes the current [WebView] to abort loading the URL, while returning + /// false causes the [WebView] to continue loading the URL as usual. + void requestLoading(WebView webView, WebResourceRequest request) {} + + // TODO(bparrishMines): Update documentation once synchronous url handling is supported. + /// When a URL is about to be loaded in the current [WebView]. + /// + /// If a [WebViewClient] is not provided, by default [WebView] will ask + /// Activity Manager to choose the proper handler for the URL. If a + /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true + /// causes the current [WebView] to abort loading the URL, while returning + /// false causes the [WebView] to continue loading the URL as usual. + void urlLoading(WebView webView, String url) {} +} + +/// The interface to be used when content can not be handled by the rendering engine for [WebView], and should be downloaded instead. +abstract class DownloadListener { + /// Pigeon Host Api implementation for [DownloadListener]. + @visibleForTesting + static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); + + /// Notify the host application that a file should be downloaded. + void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ); +} + +/// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. +abstract class WebChromeClient { + /// Pigeon Host Api implementation for [WebChromeClient]. + @visibleForTesting + static WebChromeClientHostApiImpl api = WebChromeClientHostApiImpl(); + + /// Notify the host application that a file should be downloaded. + void onProgressChanged(WebView webView, int progress); +} + +/// Encompasses parameters to the [WebViewClient.requestLoading] method. +class WebResourceRequest { + /// Constructs a [WebResourceRequest]. + WebResourceRequest({ + required this.url, + required this.isForMainFrame, + required this.isRedirect, + required this.hasGesture, + required this.method, + required this.requestHeaders, + }); + + /// Gets the URL for which the resource request was made. + final String url; + + /// Gets whether the request was made in order to fetch the main frame's document. + final isForMainFrame; + + /// Gets whether the request was a result of a server-side redirect. + /// + /// Only supported on Android version >= 24. + final bool? isRedirect; + + /// Gets whether a gesture (such as a click) was associated with the request. + final bool hasGesture; + + /// Gets the method associated with the request, for example "GET". + final String method; + + /// Gets the headers associated with the request. + final Map requestHeaders; +} + +/// Encapsulates information about errors occurred during loading of web resources. +/// +/// See [WebViewClient.onReceivedRequestError]. +class WebResourceError { + /// Constructs a [WebResourceError]. + WebResourceError({ + required this.errorCode, + required this.description, + }); + + /// The integer code of the error (e.g. [WebViewClient.errorAuthentication]. + final int errorCode; + + /// Describes the error. + final String description; +} 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 new file mode 100644 index 000000000000..81ec7c7bf80e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -0,0 +1,1601 @@ +// Copyright 2013 The Flutter 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.7), 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'; + +class WebResourceRequestData { + String? url; + bool? isForMainFrame; + bool? isRedirect; + bool? hasGesture; + String? method; + Map? requestHeaders; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['url'] = url; + pigeonMap['isForMainFrame'] = isForMainFrame; + pigeonMap['isRedirect'] = isRedirect; + pigeonMap['hasGesture'] = hasGesture; + pigeonMap['method'] = method; + pigeonMap['requestHeaders'] = requestHeaders; + return pigeonMap; + } + + 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(); + } +} + +class WebResourceErrorData { + int? errorCode; + String? description; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['errorCode'] = errorCode; + pigeonMap['description'] = description; + return pigeonMap; + } + + static WebResourceErrorData decode(Object message) { + final Map pigeonMap = message as Map; + return WebResourceErrorData() + ..errorCode = pigeonMap['errorCode'] as int? + ..description = pigeonMap['description'] as String?; + } +} + +class _WebViewHostApiCodec extends StandardMessageCodec { + const _WebViewHostApiCodec(); +} + +class WebViewHostApi { + /// Constructor for [WebViewHostApi]. 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. + WebViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebViewHostApiCodec(); + + Future create(int arg_instanceId, bool arg_useHybridComposition) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future loadUrl(int arg_instanceId, String arg_url, + Map arg_headers) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (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.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?)!; + } + } + + Future canGoBack(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future canGoForward(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future goBack(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future goForward(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future reload(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future clearCache(int arg_instanceId, bool arg_includeDiskFiles) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (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.WebViewHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?)!; + } + } + + 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?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?)!; + } + } + + Future scrollTo(int arg_instanceId, int arg_x, int arg_y) async { + 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?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future scrollBy(int arg_instanceId, int arg_x, int arg_y) async { + 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?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getScrollX(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future getScrollY(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future setWebContentsDebuggingEnabled(bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setWebViewClient( + int arg_instanceId, int arg_webViewClientInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future addJavaScriptChannel( + int arg_instanceId, int arg_javaScriptChannelInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future removeJavaScriptChannel( + int arg_instanceId, int arg_javaScriptChannelInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDownloadListener( + 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]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setWebChromeClient( + 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]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WebSettingsHostApiCodec extends StandardMessageCodec { + const _WebSettingsHostApiCodec(); +} + +class WebSettingsHostApi { + /// Constructor for [WebSettingsHostApi]. 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. + WebSettingsHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebSettingsHostApiCodec(); + + Future create(int arg_instanceId, int arg_webViewInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.create', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDomStorageEnabled(int arg_instanceId, bool arg_flag) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setJavaScriptCanOpenWindowsAutomatically( + int arg_instanceId, bool arg_flag) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setSupportMultipleWindows( + int arg_instanceId, bool arg_support) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (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_flag) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setUserAgentString( + 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]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setMediaPlaybackRequiresUserGesture( + int arg_instanceId, bool arg_require) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setSupportZoom(int arg_instanceId, bool arg_support) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setLoadWithOverviewMode( + int arg_instanceId, bool arg_overview) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, + binaryMessenger: _binaryMessenger); + 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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setUseWideViewPort(int arg_instanceId, bool arg_use) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDisplayZoomControls( + int arg_instanceId, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setBuiltInZoomControls( + int arg_instanceId, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _JavaScriptChannelHostApiCodec extends StandardMessageCodec { + const _JavaScriptChannelHostApiCodec(); +} + +class JavaScriptChannelHostApi { + /// Constructor for [JavaScriptChannelHostApi]. 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. + JavaScriptChannelHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _JavaScriptChannelHostApiCodec(); + + Future create(int arg_instanceId, String arg_channelName) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { + const _JavaScriptChannelFlutterApiCodec(); +} + +abstract class JavaScriptChannelFlutterApi { + static const MessageCodec codec = + _JavaScriptChannelFlutterApiCodec(); + + void postMessage(int instanceId, String message); + static void setup(JavaScriptChannelFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage 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.JavaScriptChannelFlutterApi.postMessage was null, expected non-null int.'); + final String? arg_message = args[1] as String?; + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null String.'); + api.postMessage(arg_instanceId!, arg_message!); + return; + }); + } + } + } +} + +class _WebViewClientHostApiCodec extends StandardMessageCodec { + const _WebViewClientHostApiCodec(); +} + +class WebViewClientHostApi { + /// Constructor for [WebViewClientHostApi]. 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. + WebViewClientHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebViewClientHostApiCodec(); + + Future create( + int arg_instanceId, bool arg_shouldOverrideUrlLoading) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WebViewClientFlutterApiCodec extends StandardMessageCodec { + const _WebViewClientFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WebResourceErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WebResourceRequestData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is WebResourceRequestData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WebResourceErrorData.decode(readValue(buffer)!); + + case 129: + return WebResourceRequestData.decode(readValue(buffer)!); + + case 130: + return WebResourceRequestData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class WebViewClientFlutterApi { + static const MessageCodec codec = _WebViewClientFlutterApiCodec(); + + void onPageStarted(int instanceId, int webViewInstanceId, String url); + void onPageFinished(int instanceId, int webViewInstanceId, String url); + void onReceivedRequestError(int instanceId, int webViewInstanceId, + WebResourceRequestData request, WebResourceErrorData error); + void onReceivedError(int instanceId, int webViewInstanceId, int errorCode, + String description, String failingUrl); + void requestLoading( + int instanceId, int webViewInstanceId, WebResourceRequestData request); + void urlLoading(int instanceId, int webViewInstanceId, String url); + static void setup(WebViewClientFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted 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.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); + final String? arg_url = args[2] as String?; + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null String.'); + api.onPageStarted(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished 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.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); + final String? arg_url = args[2] as String?; + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null String.'); + api.onPageFinished(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError', + codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError 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.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); + final WebResourceRequestData? arg_request = + args[2] as WebResourceRequestData?; + assert(arg_request != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceRequestData.'); + final WebResourceErrorData? arg_error = + args[3] as WebResourceErrorData?; + assert(arg_error != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceErrorData.'); + api.onReceivedRequestError(arg_instanceId!, arg_webViewInstanceId!, + arg_request!, arg_error!); + return; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError 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.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + final int? arg_errorCode = args[2] as int?; + assert(arg_errorCode != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + final String? arg_description = args[3] as String?; + assert(arg_description != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); + final String? arg_failingUrl = args[4] as String?; + assert(arg_failingUrl != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); + api.onReceivedError(arg_instanceId!, arg_webViewInstanceId!, + arg_errorCode!, arg_description!, arg_failingUrl!); + return; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading 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.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); + final WebResourceRequestData? arg_request = + args[2] as WebResourceRequestData?; + assert(arg_request != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null WebResourceRequestData.'); + api.requestLoading( + arg_instanceId!, arg_webViewInstanceId!, arg_request!); + return; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading 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.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); + final String? arg_url = args[2] as String?; + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null String.'); + api.urlLoading(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + } +} + +class _DownloadListenerHostApiCodec extends StandardMessageCodec { + const _DownloadListenerHostApiCodec(); +} + +class DownloadListenerHostApi { + /// Constructor for [DownloadListenerHostApi]. 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. + DownloadListenerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _DownloadListenerHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { + const _DownloadListenerFlutterApiCodec(); +} + +abstract class DownloadListenerFlutterApi { + static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + + void onDownloadStart(int instanceId, String url, String userAgent, + String contentDisposition, String mimetype, int contentLength); + static void setup(DownloadListenerFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', + codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart 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.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); + final String? arg_url = args[1] as String?; + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + final String? arg_userAgent = args[2] as String?; + assert(arg_userAgent != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + final String? arg_contentDisposition = args[3] as String?; + assert(arg_contentDisposition != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + final String? arg_mimetype = args[4] as String?; + assert(arg_mimetype != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + final int? arg_contentLength = args[5] as int?; + assert(arg_contentLength != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); + api.onDownloadStart(arg_instanceId!, arg_url!, arg_userAgent!, + arg_contentDisposition!, arg_mimetype!, arg_contentLength!); + return; + }); + } + } + } +} + +class _WebChromeClientHostApiCodec extends StandardMessageCodec { + const _WebChromeClientHostApiCodec(); +} + +class WebChromeClientHostApi { + /// Constructor for [WebChromeClientHostApi]. 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. + WebChromeClientHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebChromeClientHostApiCodec(); + + Future create( + int arg_instanceId, int arg_webViewClientInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { + const _WebChromeClientFlutterApiCodec(); +} + +abstract class WebChromeClientFlutterApi { + static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); + + void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + static void setup(WebChromeClientFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', + codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged 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.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + final int? arg_progress = args[2] as int?; + assert(arg_progress != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + api.onProgressChanged( + arg_instanceId!, arg_webViewInstanceId!, arg_progress!); + 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 new file mode 100644 index 000000000000..f909e49bd802 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -0,0 +1,622 @@ +// Copyright 2013 The Flutter Authors. All 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 'android_webview.dart'; +import 'android_webview.pigeon.dart'; +import 'instance_manager.dart'; + +/// Host api implementation for [WebView]. +class WebViewHostApiImpl extends WebViewHostApi { + /// Constructs a [WebViewHostApiImpl]. + WebViewHostApiImpl({ + 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(WebView instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId, instance.useHybridComposition); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebView instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future loadUrlFromInstance( + WebView instance, + String url, + Map headers, + ) { + return loadUrl(instanceManager.getInstanceId(instance)!, url, headers); + } + + /// Helper method to convert instances ids to objects. + Future getUrlFromInstance(WebView instance) { + return getUrl(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future canGoBackFromInstance(WebView instance) { + return canGoBack(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future canGoForwardFromInstance(WebView instance) { + return canGoForward(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future goBackFromInstance(WebView instance) { + return goBack(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future goForwardFromInstance(WebView instance) { + return goForward(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future reloadFromInstance(WebView instance) { + return reload(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future clearCacheFromInstance(WebView instance, bool includeDiskFiles) { + return clearCache( + instanceManager.getInstanceId(instance)!, + includeDiskFiles, + ); + } + + /// Helper method to convert instances ids to objects. + Future evaluateJavascriptFromInstance( + WebView instance, + String javascriptString, + ) { + return evaluateJavascript( + instanceManager.getInstanceId(instance)!, javascriptString); + } + + /// Helper method to convert instances ids to objects. + Future getTitleFromInstance(WebView instance) { + return getTitle(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future scrollToFromInstance(WebView instance, int x, int y) { + return scrollTo(instanceManager.getInstanceId(instance)!, x, y); + } + + /// Helper method to convert instances ids to objects. + Future scrollByFromInstance(WebView instance, int x, int y) { + return scrollBy(instanceManager.getInstanceId(instance)!, x, y); + } + + /// Helper method to convert instances ids to objects. + Future getScrollXFromInstance(WebView instance) { + return getScrollX(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future getScrollYFromInstance(WebView instance) { + return getScrollY(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future setWebViewClientFromInstance( + WebView instance, + WebViewClient webViewClient, + ) { + return setWebViewClient( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(webViewClient)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future addJavaScriptChannelFromInstance( + WebView instance, + JavaScriptChannel javaScriptChannel, + ) { + return addJavaScriptChannel( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(javaScriptChannel)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future removeJavaScriptChannelFromInstance( + WebView instance, + JavaScriptChannel javaScriptChannel, + ) { + return removeJavaScriptChannel( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(javaScriptChannel)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future setDownloadListenerFromInstance( + WebView instance, + DownloadListener listener, + ) { + return setDownloadListener( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(listener)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future setWebChromeClientFromInstance( + WebView instance, + WebChromeClient client, + ) { + return setWebChromeClient( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(client)!, + ); + } +} + +/// Host api implementation for [WebSettings]. +class WebSettingsHostApiImpl extends WebSettingsHostApi { + /// Constructs a [WebSettingsHostApiImpl]. + WebSettingsHostApiImpl({ + 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(WebSettings instance, WebView webView) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create( + instanceId, + instanceManager.getInstanceId(webView)!, + ); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebSettings instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future setDomStorageEnabledFromInstance( + WebSettings instance, + bool flag, + ) { + return setDomStorageEnabled(instanceManager.getInstanceId(instance)!, flag); + } + + /// Helper method to convert instances ids to objects. + Future setJavaScriptCanOpenWindowsAutomaticallyFromInstance( + WebSettings instance, + bool flag, + ) { + return setJavaScriptCanOpenWindowsAutomatically( + instanceManager.getInstanceId(instance)!, + flag, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSupportMultipleWindowsFromInstance( + WebSettings instance, + bool support, + ) { + return setSupportMultipleWindows( + instanceManager.getInstanceId(instance)!, support); + } + + /// Helper method to convert instances ids to objects. + Future setJavaScriptEnabledFromInstance( + WebSettings instance, + bool flag, + ) { + return setJavaScriptCanOpenWindowsAutomatically( + instanceManager.getInstanceId(instance)!, + flag, + ); + } + + /// Helper method to convert instances ids to objects. + Future setUserAgentStringFromInstance( + WebSettings instance, + String userAgentString, + ) { + return setUserAgentString( + instanceManager.getInstanceId(instance)!, + userAgentString, + ); + } + + /// Helper method to convert instances ids to objects. + Future setMediaPlaybackRequiresUserGestureFromInstance( + WebSettings instance, + bool require, + ) { + return setMediaPlaybackRequiresUserGesture( + instanceManager.getInstanceId(instance)!, + require, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSupportZoomFromInstance( + WebSettings instance, + bool support, + ) { + return setSupportZoom(instanceManager.getInstanceId(instance)!, support); + } + + /// Helper method to convert instances ids to objects. + Future setLoadWithOverviewModeFromInstance( + WebSettings instance, + bool overview, + ) { + return setLoadWithOverviewMode( + instanceManager.getInstanceId(instance)!, + overview, + ); + } + + /// Helper method to convert instances ids to objects. + Future setUseWideViewPortFromInstance( + WebSettings instance, + bool use, + ) { + return setUseWideViewPort(instanceManager.getInstanceId(instance)!, use); + } + + /// Helper method to convert instances ids to objects. + Future setDisplayZoomControlsFromInstance( + WebSettings instance, + bool enabled, + ) { + return setDisplayZoomControls( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } + + /// Helper method to convert instances ids to objects. + Future setBuiltInZoomControlsFromInstance( + WebSettings instance, + bool enabled, + ) { + return setBuiltInZoomControls( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } +} + +/// Host api implementation for [JavaScriptChannel]. +class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { + /// Constructs a [JavaScriptChannelHostApiImpl]. + JavaScriptChannelHostApiImpl({ + 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(JavaScriptChannel instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId, instance.channelName); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(JavaScriptChannel instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } +} + +/// Flutter api implementation for [JavaScriptChannel]. +class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { + /// Constructs a [JavaScriptChannelFlutterApiImpl]. + JavaScriptChannelHostApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void postMessage(int instanceId, String message) { + final JavaScriptChannel instance = + instanceManager.getInstance(instanceId) as JavaScriptChannel; + instance.postMessage(message); + } +} + +/// Host api implementation for [WebViewClient]. +class WebViewClientHostApiImpl extends WebViewClientHostApi { + /// Constructs a [WebViewClientHostApiImpl]. + WebViewClientHostApiImpl({ + 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(WebViewClient instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId, instance.shouldOverrideUrlLoading); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebViewClient instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } +} + +/// Flutter api implementation for [WebViewClient]. +class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { + /// Constructs a [WebViewClientFlutterApiImpl]. + WebViewClientFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void onPageFinished(int instanceId, int webViewInstanceId, String url) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.onPageFinished( + instanceManager.getInstance(webViewInstanceId) as WebView, + url, + ); + } + + @override + void onPageStarted(int instanceId, int webViewInstanceId, String url) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.onPageStarted( + instanceManager.getInstance(webViewInstanceId) as WebView, + url, + ); + } + + @override + void onReceivedError( + int instanceId, + int webViewInstanceId, + int errorCode, + String description, + String failingUrl, + ) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + // ignore: deprecated_member_use_from_same_package + instance.onReceivedError( + instanceManager.getInstance(webViewInstanceId) as WebView, + errorCode, + description, + failingUrl, + ); + } + + @override + void onReceivedRequestError( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + WebResourceErrorData error, + ) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.onReceivedRequestError( + instanceManager.getInstance(webViewInstanceId) as WebView, + WebResourceRequest( + url: request.url!, + isForMainFrame: request.isForMainFrame!, + isRedirect: request.isRedirect, + hasGesture: request.hasGesture!, + method: request.method!, + requestHeaders: request.requestHeaders!.cast(), + ), + WebResourceError( + errorCode: error.errorCode!, + description: error.description!, + ), + ); + } + + @override + void requestLoading( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + ) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.requestLoading( + instanceManager.getInstance(webViewInstanceId) as WebView, + WebResourceRequest( + url: request.url!, + isForMainFrame: request.isForMainFrame!, + isRedirect: request.isRedirect, + hasGesture: request.hasGesture!, + method: request.method!, + requestHeaders: request.requestHeaders!.cast(), + ), + ); + } + + @override + void urlLoading( + int instanceId, + int webViewInstanceId, + String url, + ) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.urlLoading( + instanceManager.getInstance(webViewInstanceId) as WebView, + url, + ); + } +} + +/// Host api implementation for [DownloadListener]. +class DownloadListenerHostApiImpl extends DownloadListenerHostApi { + /// Constructs a [DownloadListenerHostApiImpl]. + DownloadListenerHostApiImpl({ + 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(DownloadListener instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(DownloadListener instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } +} + +/// Flutter api implementation for [DownloadListener]. +class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { + /// Constructs a [DownloadListenerFlutterApiImpl]. + DownloadListenerFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void onDownloadStart( + int instanceId, + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + final DownloadListener instance = + instanceManager.getInstance(instanceId) as DownloadListener; + instance.onDownloadStart( + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + ); + } +} + +/// Host api implementation for [DownloadListener]. +class WebChromeClientHostApiImpl extends WebChromeClientHostApi { + /// Constructs a [WebChromeClientHostApiImpl]. + WebChromeClientHostApiImpl({ + 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( + WebChromeClient instance, + WebViewClient webViewClient, + ) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId, instanceManager.getInstanceId(webViewClient)!); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebChromeClient instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } +} + +/// Flutter api implementation for [DownloadListener]. +class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + /// Constructs a [DownloadListenerFlutterApiImpl]. + WebChromeClientFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void onProgressChanged(int instanceId, int webViewInstanceId, int progress) { + final WebChromeClient instance = + instanceManager.getInstance(instanceId) as WebChromeClient; + instance.onProgressChanged( + instanceManager.getInstance(webViewInstanceId) as WebView, + progress, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart new file mode 100644 index 000000000000..94d8bc3253eb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/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 java objects. +class InstanceManager { + Map _instanceIdsToInstances = {}; + 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(Object instance) { + final int? instanceId = _instancesToInstanceIds[instance]; + if (instanceId != null) { + _instancesToInstanceIds.remove(instance); + _instanceIdsToInstances.remove(instanceId); + } + return instanceId; + } + + /// Retrieve the Object paired with instanceId. + Object? getInstance(int instanceId) { + return _instanceIdsToInstances[instanceId]; + } + + /// Retrieve the instanceId paired with instance. + int? getInstanceId(Object instance) { + return _instancesToInstanceIds[instance]; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart new file mode 100644 index 000000000000..1877c1b91feb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -0,0 +1,181 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +class WebResourceRequestData { + String? url; + bool? isForMainFrame; + bool? isRedirect; + bool? hasGesture; + String? method; + Map? requestHeaders; +} + +class WebResourceErrorData { + int? errorCode; + String? description; +} + +@HostApi(dartHostTestHandler: 'TestWebViewHostApi') +abstract class WebViewHostApi { + void create(int instanceId, bool useHybridComposition); + + void dispose(int instanceId); + + void loadUrl( + int instanceId, + String url, + Map headers, + ); + + 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); + + @async + String 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); + + int getScrollY(int instanceId); + + void setWebContentsDebuggingEnabled(bool enabled); + + 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); +} + +@HostApi(dartHostTestHandler: 'TestWebSettingsHostApi') +abstract class WebSettingsHostApi { + void create(int instanceId, int webViewInstanceId); + + void dispose(int instanceId); + + void setDomStorageEnabled(int instanceId, bool flag); + + 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 setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + + void setSupportZoom(int instanceId, bool support); + + void setLoadWithOverviewMode(int instanceId, bool overview); + + void setUseWideViewPort(int instanceId, bool use); + + void setDisplayZoomControls(int instanceId, bool enabled); + + void setBuiltInZoomControls(int instanceId, bool enabled); +} + +@HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') +abstract class JavaScriptChannelHostApi { + void create(int instanceId, String channelName); + + void dispose(int instanceId); +} + +@FlutterApi() +abstract class JavaScriptChannelFlutterApi { + void postMessage(int instanceId, String message); +} + +@HostApi(dartHostTestHandler: 'TestWebViewClientHostApi') +abstract class WebViewClientHostApi { + void create(int instanceId, bool shouldOverrideUrlLoading); + + void dispose(int instanceId); +} + +@FlutterApi() +abstract class WebViewClientFlutterApi { + void onPageStarted(int instanceId, int webViewInstanceId, String url); + + void onPageFinished(int instanceId, int webViewInstanceId, String url); + + void onReceivedRequestError( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + WebResourceErrorData error, + ); + + void onReceivedError( + int instanceId, + int webViewInstanceId, + int errorCode, + String description, + String failingUrl, + ); + + void requestLoading( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + ); + + void urlLoading(int instanceId, int webViewInstanceId, String url); +} + +@HostApi(dartHostTestHandler: 'TestDownloadListenerHostApi') +abstract class DownloadListenerHostApi { + void create(int instanceId); + void dispose(int instanceId); +} + +@FlutterApi() +abstract class DownloadListenerFlutterApi { + void onDownloadStart( + int instanceId, + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ); +} + +@HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') +abstract class WebChromeClientHostApi { + void create(int instanceId, int webViewClientInstanceId); + void dispose(int instanceId); +} + +@FlutterApi() +abstract class WebChromeClientFlutterApi { + void onProgressChanged(int instanceId, int webViewInstanceId, int progress); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 36f186087c08..04511e670d4c 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -27,5 +27,6 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + pigeon: 1.0.7 pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart new file mode 100644 index 000000000000..70aa53ca2610 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -0,0 +1,1000 @@ +// Copyright 2013 The Flutter 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.7), 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, 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 '../lib/src/android_webview.pigeon.dart'; + +class _TestWebViewHostApiCodec extends StandardMessageCodec { + const _TestWebViewHostApiCodec(); +} + +abstract class TestWebViewHostApi { + static const MessageCodec codec = _TestWebViewHostApiCodec(); + + void create(int instanceId, bool useHybridComposition); + void dispose(int instanceId); + void loadUrl(int instanceId, String url, Map headers); + 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); + void scrollTo(int instanceId, int x, int y); + void scrollBy(int instanceId, int x, int y); + int getScrollX(int instanceId); + int getScrollY(int instanceId); + void setWebContentsDebuggingEnabled(bool enabled); + 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); + static void setup(TestWebViewHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.create was null, expected non-null int.'); + final bool? arg_useHybridComposition = args[1] as bool?; + assert(arg_useHybridComposition != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); + api.create(arg_instanceId!, arg_useHybridComposition!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl 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.WebViewHostApi.loadUrl was null, expected non-null int.'); + final String? arg_url = args[1] as String?; + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null String.'); + final Map? arg_headers = + args[2] as Map?; + assert(arg_headers != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); + api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.getUrl was null, expected non-null int.'); + final String output = api.getUrl(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.canGoBack was null, expected non-null int.'); + final bool output = api.canGoBack(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.canGoForward was null, expected non-null int.'); + final bool output = api.canGoForward(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goBack', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.goBack was null, expected non-null int.'); + api.goBack(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goForward', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.goForward was null, expected non-null int.'); + api.goForward(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.reload', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.reload was null, expected non-null int.'); + api.reload(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache 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.WebViewHostApi.clearCache was null, expected non-null int.'); + final bool? arg_includeDiskFiles = args[1] as bool?; + assert(arg_includeDiskFiles != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); + api.clearCache(arg_instanceId!, arg_includeDiskFiles!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.evaluateJavascript was null, expected non-null int.'); + 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( + arg_instanceId!, arg_javascriptString!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.getTitle was null, expected non-null int.'); + final String output = api.getTitle(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo 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.WebViewHostApi.scrollTo was null, expected non-null int.'); + final int? arg_x = args[1] as int?; + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); + final int? arg_y = args[2] as int?; + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); + api.scrollTo(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.scrollBy was null, expected non-null int.'); + final int? arg_x = args[1] as int?; + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); + final int? arg_y = args[2] as int?; + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); + api.scrollBy(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX 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.WebViewHostApi.getScrollX was null, expected non-null int.'); + final int output = api.getScrollX(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY 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.WebViewHostApi.getScrollY was null, expected non-null int.'); + final int output = api.getScrollY(arg_instanceId!); + return {'result': output}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null.'); + final List args = (message as List?)!; + final bool? arg_enabled = args[0] as bool?; + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); + api.setWebContentsDebuggingEnabled(arg_enabled!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient 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.WebViewHostApi.setWebViewClient was null, expected non-null int.'); + final int? arg_webViewClientInstanceId = args[1] as int?; + assert(arg_webViewClientInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); + api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel 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.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); + final int? arg_javaScriptChannelInstanceId = args[1] as int?; + assert(arg_javaScriptChannelInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); + api.addJavaScriptChannel( + arg_instanceId!, arg_javaScriptChannelInstanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel 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.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); + final int? arg_javaScriptChannelInstanceId = args[1] as int?; + assert(arg_javaScriptChannelInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); + api.removeJavaScriptChannel( + arg_instanceId!, arg_javaScriptChannelInstanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener 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.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!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient 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.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!); + return {}; + }); + } + } + } +} + +class _TestWebSettingsHostApiCodec extends StandardMessageCodec { + const _TestWebSettingsHostApiCodec(); +} + +abstract class TestWebSettingsHostApi { + static const MessageCodec codec = _TestWebSettingsHostApiCodec(); + + void create(int instanceId, int webViewInstanceId); + void dispose(int instanceId); + void setDomStorageEnabled(int instanceId, bool flag); + 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 setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + void setSupportZoom(int instanceId, bool support); + void setLoadWithOverviewMode(int instanceId, bool overview); + void setUseWideViewPort(int instanceId, bool use); + void setDisplayZoomControls(int instanceId, bool enabled); + void setBuiltInZoomControls(int instanceId, bool enabled); + static void setup(TestWebSettingsHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.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.WebSettingsHostApi.create was null, expected non-null int.'); + final int? arg_webViewInstanceId = args[1] as int?; + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!, arg_webViewInstanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.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.WebSettingsHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled 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.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null int.'); + final bool? arg_flag = args[1] as bool?; + assert(arg_flag != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); + api.setDomStorageEnabled(arg_instanceId!, arg_flag!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically 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.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null int.'); + final bool? arg_flag = args[1] as bool?; + assert(arg_flag != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); + api.setJavaScriptCanOpenWindowsAutomatically( + arg_instanceId!, arg_flag!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows 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.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null int.'); + final bool? arg_support = args[1] as bool?; + assert(arg_support != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); + api.setSupportMultipleWindows(arg_instanceId!, arg_support!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.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.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null int.'); + final bool? arg_flag = args[1] as bool?; + assert(arg_flag != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); + api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString 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.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!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture 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.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null int.'); + final bool? arg_require = args[1] as bool?; + assert(arg_require != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); + api.setMediaPlaybackRequiresUserGesture( + arg_instanceId!, arg_require!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom 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.WebSettingsHostApi.setSupportZoom was null, expected non-null int.'); + final bool? arg_support = args[1] as bool?; + assert(arg_support != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); + api.setSupportZoom(arg_instanceId!, arg_support!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode 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.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null int.'); + final bool? arg_overview = args[1] as bool?; + assert(arg_overview != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); + api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort 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.WebSettingsHostApi.setUseWideViewPort was null, expected non-null int.'); + final bool? arg_use = args[1] as bool?; + assert(arg_use != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); + api.setUseWideViewPort(arg_instanceId!, arg_use!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls 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.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null int.'); + final bool? arg_enabled = args[1] as bool?; + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); + api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', + codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls 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.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null int.'); + final bool? arg_enabled = args[1] as bool?; + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); + api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + } +} + +class _TestJavaScriptChannelHostApiCodec extends StandardMessageCodec { + const _TestJavaScriptChannelHostApiCodec(); +} + +abstract class TestJavaScriptChannelHostApi { + static const MessageCodec codec = + _TestJavaScriptChannelHostApiCodec(); + + void create(int instanceId, String channelName); + void dispose(int instanceId); + static void setup(TestJavaScriptChannelHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.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.JavaScriptChannelHostApi.create was null, expected non-null int.'); + final String? arg_channelName = args[1] as String?; + assert(arg_channelName != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); + api.create(arg_instanceId!, arg_channelName!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.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.JavaScriptChannelHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWebViewClientHostApiCodec extends StandardMessageCodec { + const _TestWebViewClientHostApiCodec(); +} + +abstract class TestWebViewClientHostApi { + static const MessageCodec codec = _TestWebViewClientHostApiCodec(); + + void create(int instanceId, bool shouldOverrideUrlLoading); + void dispose(int instanceId); + static void setup(TestWebViewClientHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientHostApi.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.WebViewClientHostApi.create was null, expected non-null int.'); + final bool? arg_shouldOverrideUrlLoading = args[1] as bool?; + assert(arg_shouldOverrideUrlLoading != null, + 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null bool.'); + api.create(arg_instanceId!, arg_shouldOverrideUrlLoading!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientHostApi.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.WebViewClientHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestDownloadListenerHostApiCodec extends StandardMessageCodec { + const _TestDownloadListenerHostApiCodec(); +} + +abstract class TestDownloadListenerHostApi { + static const MessageCodec codec = + _TestDownloadListenerHostApiCodec(); + + void create(int instanceId); + void dispose(int instanceId); + static void setup(TestDownloadListenerHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.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.DownloadListenerHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.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.DownloadListenerHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWebChromeClientHostApiCodec extends StandardMessageCodec { + const _TestWebChromeClientHostApiCodec(); +} + +abstract class TestWebChromeClientHostApi { + static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); + + void create(int instanceId, int webViewClientInstanceId); + void dispose(int instanceId); + static void setup(TestWebChromeClientHostApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.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.WebChromeClientHostApi.create was null, expected non-null int.'); + final int? arg_webViewClientInstanceId = args[1] as int?; + assert(arg_webViewClientInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!, arg_webViewClientInstanceId!); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.dispose', codec); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.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.WebChromeClientHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + } +} 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 new file mode 100644 index 000000000000..462670abdb83 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -0,0 +1,373 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_android/src/android_webview.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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Android WebView', () { + group('$WebView', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + }); + + setUp(() { + WebView.api = WebViewHostApiImpl(instanceManager: InstanceManager()); + }); + + test('create', () { + final WebView webView = WebView(); + expect(WebView.api.instanceManager.getInstanceId(webView), isNotNull); + }); + }); + + group('$WebSettings', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestWebSettingsHostApi.setup(TestWebSettingsHostApiImpl()); + }); + + setUp(() { + final InstanceManager instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + }); + + test('create', () { + final WebView webView = WebView(); + final WebSettings webSettings = WebSettings(webView); + expect( + WebSettings.api.instanceManager.getInstanceId(webSettings), + isNotNull, + ); + }); + }); + + group('$JavaScriptChannel', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestJavaScriptChannelHostApi.setup(TestJavaScriptChannelHostApiImpl()); + }); + + setUp(() { + final InstanceManager instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + JavaScriptChannel.api = JavaScriptChannelHostApiImpl( + instanceManager: instanceManager, + ); + }); + + test('create', () { + final WebView webView = WebView(); + final JavaScriptChannel channel = TestJavaScriptChannel('myChannel'); + + webView.addJavaScriptChannel(channel); + expect( + JavaScriptChannel.api.instanceManager.getInstanceId(channel), + isNotNull, + ); + + webView.removeJavaScriptChannel(channel); + expect( + JavaScriptChannel.api.instanceManager.getInstanceId(channel), + isNull, + ); + }); + }); + + group('$WebViewClient', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); + }); + + setUp(() { + final InstanceManager instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + }); + + test('create', () { + final WebView webView = WebView(); + final WebViewClient webViewClient1 = TestWebViewClient(); + final WebViewClient webViewClient2 = TestWebViewClient(); + + webView.setWebViewClient(webViewClient1); + expect( + WebViewClient.api.instanceManager.getInstanceId(webViewClient1), + isNotNull, + ); + + webView.setWebViewClient(webViewClient2); + expect( + WebViewClient.api.instanceManager.getInstanceId(webViewClient1), + isNull, + ); + }); + }); + + group('$DownloadListener', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestDownloadListenerHostApi.setup(TestDownloadListenerHostApiImpl()); + }); + + setUp(() { + final InstanceManager instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + DownloadListener.api = DownloadListenerHostApiImpl( + instanceManager: instanceManager, + ); + }); + + test('create', () { + final WebView webView = WebView(); + final DownloadListener downloadListener1 = TestDownloadListener(); + final DownloadListener downloadListener2 = TestDownloadListener(); + + webView.setDownloadListener(downloadListener1); + expect( + DownloadListener.api.instanceManager.getInstanceId(downloadListener1), + isNotNull, + ); + + webView.setDownloadListener(downloadListener2); + expect( + DownloadListener.api.instanceManager.getInstanceId(downloadListener1), + isNull, + ); + }); + }); + + group('$WebChromeClient', () { + setUpAll(() { + TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); + TestWebChromeClientHostApi.setup(TestWebChromeClientHostApiImpl()); + }); + + setUp(() { + final InstanceManager instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + }); + + test('create', () { + final WebView webView = WebView(); + webView.setWebViewClient(TestWebViewClient()); + + final WebChromeClient webChromeClient1 = TestWebChromeClient(); + final WebChromeClient webChromeClient2 = TestWebChromeClient(); + + webView.setWebChromeClient(webChromeClient1); + expect( + WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), + isNotNull, + ); + + webView.setWebChromeClient(webChromeClient2); + expect( + WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), + isNull, + ); + }); + }); + }); +} + +class TestJavaScriptChannel extends JavaScriptChannel { + TestJavaScriptChannel(String channelName) : super(channelName); + + @override + void postMessage(String message) {} +} + +class TestWebViewClient extends WebViewClient {} + +class TestDownloadListener extends DownloadListener { + @override + void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) {} +} + +class TestWebChromeClient extends WebChromeClient { + @override + void onProgressChanged(WebView webView, int progress) {} +} + +class TestWebViewHostApiImpl extends TestWebViewHostApi { + @override + void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId) {} + + @override + bool canGoBack(int instanceId) { + throw UnimplementedError(); + } + + @override + bool canGoForward(int instanceId) { + throw UnimplementedError(); + } + + @override + void clearCache(int instanceId, bool includeDiskFiles) {} + + @override + void create(int instanceId, bool useHybridComposition) {} + + @override + void dispose(int instanceId) {} + + @override + Future evaluateJavascript(int instanceId, String javascriptString) { + throw UnimplementedError(); + } + + @override + int getScrollX(int instanceId) { + throw UnimplementedError(); + } + + @override + int getScrollY(int instanceId) { + throw UnimplementedError(); + } + + @override + String getTitle(int instanceId) { + throw UnimplementedError(); + } + + @override + String getUrl(int instanceId) { + throw UnimplementedError(); + } + + @override + void goBack(int instanceId) {} + + @override + void goForward(int instanceId) {} + + @override + void loadUrl(int instanceId, String url, Map headers) {} + + @override + void reload(int instanceId) {} + + @override + void removeJavaScriptChannel( + int instanceId, int javaScriptChannelInstanceId) {} + + @override + void scrollBy(int instanceId, int x, int y) {} + + @override + void scrollTo(int instanceId, int x, int y) {} + + @override + void setDownloadListener(int instanceId, int listenerInstanceId) {} + + @override + void setWebContentsDebuggingEnabled(bool enabled) {} + + @override + void setWebViewClient(int instanceId, int webViewClientInstanceId) {} + + @override + void setWebChromeClient(int instanceId, int clientInstanceId) {} +} + +class TestWebSettingsHostApiImpl extends TestWebSettingsHostApi { + @override + void create(int instanceId, int webViewInstanceId) {} + + @override + void dispose(int instanceId) {} + + @override + void setBuiltInZoomControls(int instanceId, bool enabled) {} + + @override + void setDisplayZoomControls(int instanceId, bool enabled) {} + + @override + void setDomStorageEnabled(int instanceId, bool flag) {} + + @override + void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag) {} + + @override + void setJavaScriptEnabled(int instanceId, bool flag) {} + + @override + void setLoadWithOverviewMode(int instanceId, bool overview) {} + + @override + void setMediaPlaybackRequiresUserGesture(int instanceId, bool require) {} + + @override + void setSupportMultipleWindows(int instanceId, bool support) {} + + @override + void setSupportZoom(int instanceId, bool support) {} + + @override + void setUseWideViewPort(int instanceId, bool use) {} + + @override + void setUserAgentString(int instanceId, String userAgentString) {} +} + +class TestJavaScriptChannelHostApiImpl extends TestJavaScriptChannelHostApi { + @override + void create(int instanceId, String channelName) {} + + @override + void dispose(int instanceId) {} +} + +class TestWebViewClientHostApiImpl extends TestWebViewClientHostApi { + @override + void create(int instanceId, bool shouldOverrideUrlLoading) {} + + @override + void dispose(int instanceId) {} +} + +class TestDownloadListenerHostApiImpl extends TestDownloadListenerHostApi { + @override + void create(int instanceId) {} + + @override + void dispose(int instanceId) {} +} + +class TestWebChromeClientHostApiImpl extends TestWebChromeClientHostApi { + @override + void create(int instanceId, int webViewClientInstanceId) {} + + @override + void dispose(int instanceId) {} +} diff --git a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart new file mode 100644 index 000000000000..fd020fc362c8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/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_android/src/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); + }); + }); +} From 0d330943941a7ba943305063f1733340a916deca Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 26 Oct 2021 20:49:31 -0700 Subject: [PATCH 119/259] [in_app_purchase] Update to the latest pkg:json_serializable (#4434) - Remove unneeded class now that enum helpers can be generated without one - Code reorganization - Remove superflous JsonKey values --- .../in_app_purchase/build.yaml | 7 - .../in_app_purchase_android/CHANGELOG.md | 6 + .../in_app_purchase_android/build.yaml | 3 +- .../billing_client_wrapper.dart | 92 ++++++++++- .../billing_client_wrapper.g.dart | 43 ++++++ .../enum_converters.dart | 143 ------------------ .../enum_converters.g.dart | 94 ------------ .../purchase_wrapper.dart | 40 ++++- .../purchase_wrapper.g.dart | 49 +----- .../sku_details_wrapper.dart | 4 +- .../sku_details_wrapper.g.dart | 35 ----- .../types/google_play_purchase_details.dart | 1 - .../in_app_purchase_android/pubspec.yaml | 8 +- .../billing_client_wrapper_test.dart | 5 +- .../purchase_wrapper_test.dart | 1 - .../sku_details_wrapper_test.dart | 3 +- ...rchase_android_platform_addition_test.dart | 1 - ...in_app_purchase_android_platform_test.dart | 1 - .../in_app_purchase_ios/CHANGELOG.md | 4 + .../in_app_purchase_ios/build.yaml | 3 +- .../store_kit_wrappers/enum_converters.dart | 6 +- .../store_kit_wrappers/enum_converters.g.dart | 40 +---- .../sk_payment_queue_wrapper.dart | 3 +- .../sk_payment_queue_wrapper.g.dart | 6 - .../sk_payment_transaction_wrappers.dart | 8 +- .../sk_product_wrapper.g.dart | 45 ------ .../sk_storefront_wrapper.dart | 2 +- .../in_app_purchase_ios/pubspec.yaml | 6 +- 28 files changed, 216 insertions(+), 443 deletions(-) delete mode 100644 packages/in_app_purchase/in_app_purchase/build.yaml create mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart delete mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart delete mode 100644 packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart diff --git a/packages/in_app_purchase/in_app_purchase/build.yaml b/packages/in_app_purchase/in_app_purchase/build.yaml deleted file mode 100644 index e15cf14b85fd..000000000000 --- a/packages/in_app_purchase/in_app_purchase/build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - any_map: true - create_to_json: true 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 a01eb9f8b70a..c504bd76cdc5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,6 +1,12 @@ +## 0.1.6 + +* Require Dart SDK >= 2.14. +* Update `json_annotation` dependency to `^4.3.0`. + ## 0.1.5+1 * Fix a broken link in the README. + ## 0.1.5 * Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field. diff --git a/packages/in_app_purchase/in_app_purchase_android/build.yaml b/packages/in_app_purchase/in_app_purchase_android/build.yaml index e15cf14b85fd..651a557fc1ca 100644 --- a/packages/in_app_purchase/in_app_purchase_android/build.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/build.yaml @@ -1,7 +1,8 @@ +# See https://pub.dev/packages/build_config targets: $default: builders: json_serializable: options: any_map: true - create_to_json: true + create_to_json: false 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 4393d1d72eaf..0e7024a59aaa 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 @@ -3,14 +3,17 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:flutter/services.dart'; + import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:json_annotation/json_annotation.dart'; + import '../../billing_client_wrappers.dart'; import '../channel.dart'; import 'purchase_wrapper.dart'; import 'sku_details_wrapper.dart'; -import 'enum_converters.dart'; + +part 'billing_client_wrapper.g.dart'; /// Method identifier for the OnPurchaseUpdated method channel method. @visibleForTesting @@ -364,6 +367,7 @@ typedef void OnBillingServiceDisconnected(); /// [`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). /// See the `BillingResponse` docs for more explanation of the different /// constants. +@JsonEnum(alwaysCreate: true) enum BillingResponse { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -418,11 +422,32 @@ enum BillingResponse { itemNotOwned, } +/// Serializer for [BillingResponse]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingResponseConverter()`. +class BillingResponseConverter implements JsonConverter { + /// Default const constructor. + const BillingResponseConverter(); + + @override + BillingResponse fromJson(int? json) { + if (json == null) { + return BillingResponse.error; + } + return $enumDecode(_$BillingResponseEnumMap, json); + } + + @override + int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; +} + /// Enum representing potential [SkuDetailsWrapper.type]s. /// /// Wraps /// [`BillingClient.SkuType`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType) /// See the linked documentation for an explanation of the different constants. +@JsonEnum(alwaysCreate: true) enum SkuType { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -437,6 +462,26 @@ enum SkuType { subs, } +/// Serializer for [SkuType]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SkuTypeConverter()`. +class SkuTypeConverter implements JsonConverter { + /// Default const constructor. + const SkuTypeConverter(); + + @override + SkuType fromJson(String? json) { + if (json == null) { + return SkuType.inapp; + } + return $enumDecode(_$SkuTypeEnumMap, json); + } + + @override + String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; +} + /// Enum representing the proration mode. /// /// When upgrading or downgrading a subscription, set this mode to provide details @@ -444,6 +489,7 @@ enum SkuType { /// /// Wraps [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode) /// See the linked documentation for an explanation of the different constants. +@JsonEnum(alwaysCreate: true) enum ProrationMode { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -477,7 +523,28 @@ enum ProrationMode { deferred, } +/// Serializer for [ProrationMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@ProrationModeConverter()`. +class ProrationModeConverter implements JsonConverter { + /// Default const constructor. + const ProrationModeConverter(); + + @override + ProrationMode fromJson(int? json) { + if (json == null) { + return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; + } + return $enumDecode(_$ProrationModeEnumMap, json); + } + + @override + int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; +} + /// Features/capabilities supported by [BillingClient.isFeatureSupported()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType). +@JsonEnum(alwaysCreate: true) enum BillingClientFeature { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -504,3 +571,24 @@ enum BillingClientFeature { @JsonValue('subscriptionsUpdate') subscriptionsUpdate } + +/// Serializer for [BillingClientFeature]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingClientFeatureConverter()`. +class BillingClientFeatureConverter + implements JsonConverter { + /// Default const constructor. + const BillingClientFeatureConverter(); + + @override + BillingClientFeature fromJson(String json) { + return $enumDecode( + _$BillingClientFeatureEnumMap.cast(), + json); + } + + @override + String toJson(BillingClientFeature object) => + _$BillingClientFeatureEnumMap[object]!; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart new file mode 100644 index 000000000000..efe7656d2138 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'billing_client_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +const _$BillingResponseEnumMap = { + BillingResponse.serviceTimeout: -3, + BillingResponse.featureNotSupported: -2, + BillingResponse.serviceDisconnected: -1, + BillingResponse.ok: 0, + BillingResponse.userCanceled: 1, + BillingResponse.serviceUnavailable: 2, + BillingResponse.billingUnavailable: 3, + BillingResponse.itemUnavailable: 4, + BillingResponse.developerError: 5, + BillingResponse.error: 6, + BillingResponse.itemAlreadyOwned: 7, + BillingResponse.itemNotOwned: 8, +}; + +const _$SkuTypeEnumMap = { + SkuType.inapp: 'inapp', + SkuType.subs: 'subs', +}; + +const _$ProrationModeEnumMap = { + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, + ProrationMode.immediateWithTimeProration: 1, + ProrationMode.immediateAndChargeProratedPrice: 2, + ProrationMode.immediateWithoutProration: 3, + ProrationMode.deferred: 4, +}; + +const _$BillingClientFeatureEnumMap = { + BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', + BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', + BillingClientFeature.subscriptions: 'subscriptions', + BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', + BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', +}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart deleted file mode 100644 index 931d92f7b1e7..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import '../../billing_client_wrappers.dart'; - -part 'enum_converters.g.dart'; - -/// Serializer for [BillingResponse]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingResponseConverter()`. -class BillingResponseConverter implements JsonConverter { - /// Default const constructor. - const BillingResponseConverter(); - - @override - BillingResponse fromJson(int? json) { - if (json == null) { - return BillingResponse.error; - } - return _$enumDecode( - _$BillingResponseEnumMap.cast(), json); - } - - @override - int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; -} - -/// Serializer for [SkuType]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SkuTypeConverter()`. -class SkuTypeConverter implements JsonConverter { - /// Default const constructor. - const SkuTypeConverter(); - - @override - SkuType fromJson(String? json) { - if (json == null) { - return SkuType.inapp; - } - return _$enumDecode( - _$SkuTypeEnumMap.cast(), json); - } - - @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; -} - -/// Serializer for [ProrationMode]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@ProrationModeConverter()`. -class ProrationModeConverter implements JsonConverter { - /// Default const constructor. - const ProrationModeConverter(); - - @override - ProrationMode fromJson(int? json) { - if (json == null) { - return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; - } - return _$enumDecode( - _$ProrationModeEnumMap.cast(), json); - } - - @override - int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; -} - -/// Serializer for [PurchaseStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@PurchaseStateConverter()`. -class PurchaseStateConverter - implements JsonConverter { - /// Default const constructor. - const PurchaseStateConverter(); - - @override - PurchaseStateWrapper fromJson(int? json) { - if (json == null) { - return PurchaseStateWrapper.unspecified_state; - } - return _$enumDecode( - _$PurchaseStateWrapperEnumMap.cast(), - json); - } - - @override - int toJson(PurchaseStateWrapper object) => - _$PurchaseStateWrapperEnumMap[object]!; - - /// Converts the purchase state stored in `object` to a [PurchaseStatus]. - /// - /// [PurchaseStateWrapper.unspecified_state] is mapped to [PurchaseStatus.error]. - PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { - switch (object) { - case PurchaseStateWrapper.pending: - return PurchaseStatus.pending; - case PurchaseStateWrapper.purchased: - return PurchaseStatus.purchased; - case PurchaseStateWrapper.unspecified_state: - return PurchaseStatus.error; - } - } -} - -/// Serializer for [BillingClientFeature]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingClientFeatureConverter()`. -class BillingClientFeatureConverter - implements JsonConverter { - /// Default const constructor. - const BillingClientFeatureConverter(); - - @override - BillingClientFeature fromJson(String json) { - return _$enumDecode( - _$BillingClientFeatureEnumMap.cast(), - json); - } - - @override - String toJson(BillingClientFeature object) => - _$BillingClientFeatureEnumMap[object]!; -} - -// Define a class so we generate serializer helper methods for the enums -// See https://github.com/google/json_serializable.dart/issues/778 -@JsonSerializable() -class _SerializedEnums { - late BillingResponse response; - late SkuType type; - late PurchaseStateWrapper purchaseState; - late ProrationMode prorationMode; - late BillingClientFeature billingClientFeature; -} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart deleted file mode 100644 index fe92f56653e4..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart +++ /dev/null @@ -1,94 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'enum_converters.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() - ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) - ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) - ..purchaseState = - _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) - ..prorationMode = _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']) - ..billingClientFeature = - _$enumDecode(_$BillingClientFeatureEnumMap, json['billingClientFeature']); - -Map _$SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$BillingResponseEnumMap[instance.response], - 'type': _$SkuTypeEnumMap[instance.type], - 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], - 'prorationMode': _$ProrationModeEnumMap[instance.prorationMode], - 'billingClientFeature': - _$BillingClientFeatureEnumMap[instance.billingClientFeature], - }; - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - -const _$BillingResponseEnumMap = { - BillingResponse.serviceTimeout: -3, - BillingResponse.featureNotSupported: -2, - BillingResponse.serviceDisconnected: -1, - BillingResponse.ok: 0, - BillingResponse.userCanceled: 1, - BillingResponse.serviceUnavailable: 2, - BillingResponse.billingUnavailable: 3, - BillingResponse.itemUnavailable: 4, - BillingResponse.developerError: 5, - BillingResponse.error: 6, - BillingResponse.itemAlreadyOwned: 7, - BillingResponse.itemNotOwned: 8, -}; - -const _$SkuTypeEnumMap = { - SkuType.inapp: 'inapp', - SkuType.subs: 'subs', -}; - -const _$PurchaseStateWrapperEnumMap = { - PurchaseStateWrapper.unspecified_state: 0, - PurchaseStateWrapper.purchased: 1, - PurchaseStateWrapper.pending: 2, -}; - -const _$ProrationModeEnumMap = { - ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, - ProrationMode.immediateWithTimeProration: 1, - ProrationMode.immediateAndChargeProratedPrice: 2, - ProrationMode.immediateWithoutProration: 3, - ProrationMode.deferred: 4, -}; - -const _$BillingClientFeatureEnumMap = { - BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', - BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', - BillingClientFeature.subscriptions: 'subscriptions', - BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', - BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', -}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 374c26ab4a7a..742288e56412 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 @@ -3,10 +3,11 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; + import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'enum_converters.dart'; + import 'billing_client_wrapper.dart'; import 'sku_details_wrapper.dart'; @@ -323,6 +324,7 @@ class PurchasesHistoryResult { /// Wraps /// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). /// * See also: [PurchaseWrapper]. +@JsonEnum(alwaysCreate: true) enum PurchaseStateWrapper { /// The state is unspecified. /// @@ -348,3 +350,39 @@ enum PurchaseStateWrapper { @JsonValue(2) pending, } + +/// Serializer for [PurchaseStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@PurchaseStateConverter()`. +class PurchaseStateConverter + implements JsonConverter { + /// Default const constructor. + const PurchaseStateConverter(); + + @override + PurchaseStateWrapper fromJson(int? json) { + if (json == null) { + return PurchaseStateWrapper.unspecified_state; + } + return $enumDecode(_$PurchaseStateWrapperEnumMap, json); + } + + @override + int toJson(PurchaseStateWrapper object) => + _$PurchaseStateWrapperEnumMap[object]!; + + /// Converts the purchase state stored in `object` to a [PurchaseStatus]. + /// + /// [PurchaseStateWrapper.unspecified_state] is mapped to [PurchaseStatus.error]. + PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { + switch (object) { + case PurchaseStateWrapper.pending: + return PurchaseStatus.pending; + case PurchaseStateWrapper.purchased: + return PurchaseStatus.purchased; + case PurchaseStateWrapper.unspecified_state: + return PurchaseStatus.error; + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index b5d9fe8cd3af..5815a866c82d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -23,24 +23,6 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( obfuscatedProfileId: json['obfuscatedProfileId'] as String?, ); -Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => - { - 'orderId': instance.orderId, - 'packageName': instance.packageName, - 'purchaseTime': instance.purchaseTime, - 'purchaseToken': instance.purchaseToken, - 'signature': instance.signature, - 'sku': instance.sku, - 'isAutoRenewing': instance.isAutoRenewing, - 'originalJson': instance.originalJson, - 'developerPayload': instance.developerPayload, - 'isAcknowledged': instance.isAcknowledged, - 'purchaseState': - const PurchaseStateConverter().toJson(instance.purchaseState), - 'obfuscatedAccountId': instance.obfuscatedAccountId, - 'obfuscatedProfileId': instance.obfuscatedProfileId, - }; - PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => PurchaseHistoryRecordWrapper( purchaseTime: json['purchaseTime'] as int? ?? 0, @@ -51,17 +33,6 @@ PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => developerPayload: json['developerPayload'] as String?, ); -Map _$PurchaseHistoryRecordWrapperToJson( - PurchaseHistoryRecordWrapper instance) => - { - 'purchaseTime': instance.purchaseTime, - 'purchaseToken': instance.purchaseToken, - 'signature': instance.signature, - 'sku': instance.sku, - 'originalJson': instance.originalJson, - 'developerPayload': instance.developerPayload, - }; - PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) => PurchasesResultWrapper( responseCode: const BillingResponseConverter() @@ -77,15 +48,6 @@ PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) => [], ); -Map _$PurchasesResultWrapperToJson( - PurchasesResultWrapper instance) => - { - 'billingResult': instance.billingResult, - 'responseCode': - const BillingResponseConverter().toJson(instance.responseCode), - 'purchasesList': instance.purchasesList, - }; - PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) => PurchasesHistoryResult( billingResult: @@ -100,9 +62,8 @@ PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) => [], ); -Map _$PurchasesHistoryResultToJson( - PurchasesHistoryResult instance) => - { - 'billingResult': instance.billingResult, - 'purchaseHistoryRecordList': instance.purchaseHistoryRecordList, - }; +const _$PurchaseStateWrapperEnumMap = { + PurchaseStateWrapper.unspecified_state: 0, + PurchaseStateWrapper.purchased: 1, + PurchaseStateWrapper.pending: 2, +}; 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 754f7a352f1c..8069a1f72281 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 @@ -3,10 +3,11 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; + import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; + import 'billing_client_wrapper.dart'; -import 'enum_converters.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -75,7 +76,6 @@ class SkuDetailsWrapper { /// /// Returns 0 if the SKU is not a subscription or doesn't have an introductory /// period. - @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: 0) final int introductoryPriceAmountMicros; /// String representation of [introductoryPrice] in micro-units 990000 diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 53d5931ecb56..05eb6bed0035 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -26,26 +26,6 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, ); -Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => - { - 'description': instance.description, - 'freeTrialPeriod': instance.freeTrialPeriod, - 'introductoryPrice': instance.introductoryPrice, - 'introductoryPriceAmountMicros': instance.introductoryPriceAmountMicros, - 'introductoryPriceCycles': instance.introductoryPriceCycles, - 'introductoryPricePeriod': instance.introductoryPricePeriod, - 'price': instance.price, - 'priceAmountMicros': instance.priceAmountMicros, - 'priceCurrencyCode': instance.priceCurrencyCode, - 'priceCurrencySymbol': instance.priceCurrencySymbol, - 'sku': instance.sku, - 'subscriptionPeriod': instance.subscriptionPeriod, - 'title': instance.title, - 'type': const SkuTypeConverter().toJson(instance.type), - 'originalPrice': instance.originalPrice, - 'originalPriceAmountMicros': instance.originalPriceAmountMicros, - }; - SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => SkuDetailsResponseWrapper( billingResult: @@ -59,24 +39,9 @@ SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => [], ); -Map _$SkuDetailsResponseWrapperToJson( - SkuDetailsResponseWrapper instance) => - { - 'billingResult': instance.billingResult, - 'skuDetailsList': instance.skuDetailsList, - }; - BillingResultWrapper _$BillingResultWrapperFromJson(Map json) => BillingResultWrapper( responseCode: const BillingResponseConverter() .fromJson(json['responseCode'] as int?), debugMessage: json['debugMessage'] as String?, ); - -Map _$BillingResultWrapperToJson( - BillingResultWrapper instance) => - { - 'responseCode': - const BillingResponseConverter().toJson(instance.responseCode), - 'debugMessage': instance.debugMessage, - }; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart index 53b58bd664fd..e607164e59f1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -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_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../../billing_client_wrappers.dart'; 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 d51abbb43edc..7230b56d5af9 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,10 +2,10 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.5+1 +version: 0.1.6 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" flutter: @@ -21,12 +21,12 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.1.0 - json_annotation: ^4.0.1 + json_annotation: ^4.3.0 meta: ^1.3.0 dev_dependencies: build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^5.0.2 + json_serializable: ^6.0.0 test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 02ae9ba33564..1a2a0e4712a7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -2,15 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import '../stub_in_app_purchase_platform.dart'; -import 'sku_details_wrapper_test.dart'; import 'purchase_wrapper_test.dart'; +import 'sku_details_wrapper_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index 70b9fcad4da7..f51342045a7d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -4,7 +4,6 @@ import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:test/test.dart'; final PurchaseWrapper dummyPurchase = PurchaseWrapper( diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart index 18804a41940e..08cde077be93 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/src/types/google_play_product_details.dart'; import 'package:test/test.dart'; -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( description: 'description', 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 a478cabac89b..ecefc4d37c84 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 @@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart' as widgets; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index 52ec08bea07a..bd704894daf8 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -9,7 +9,6 @@ import 'package:flutter/widgets.dart' as widgets; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 76cafa9201cc..4daa6a2d1dcb 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +* Require Dart SDK >= 2.14. + ## 0.1.3+5 * Updated example app to handle restored purchases properly. diff --git a/packages/in_app_purchase/in_app_purchase_ios/build.yaml b/packages/in_app_purchase/in_app_purchase_ios/build.yaml index e15cf14b85fd..651a557fc1ca 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/build.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/build.yaml @@ -1,7 +1,8 @@ +# See https://pub.dev/packages/build_config targets: $default: builders: json_serializable: options: any_map: true - create_to_json: true + create_to_json: false diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index 70178260febf..8a0526f07ad7 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -23,7 +23,7 @@ class SKTransactionStatusConverter if (json == null) { return SKPaymentTransactionStateWrapper.unspecified; } - return _$enumDecode( + return $enumDecode( _$SKPaymentTransactionStateWrapperEnumMap .cast(), json); @@ -64,7 +64,7 @@ class SKSubscriptionPeriodUnitConverter if (json == null) { return SKSubscriptionPeriodUnit.day; } - return _$enumDecode( + return $enumDecode( _$SKSubscriptionPeriodUnitEnumMap .cast(), json); @@ -89,7 +89,7 @@ class SKProductDiscountPaymentModeConverter if (json == null) { return SKProductDiscountPaymentMode.payAsYouGo; } - return _$enumDecode( + return $enumDecode( _$SKProductDiscountPaymentModeEnumMap .cast(), json); diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart index ce0f56ba4d34..0d05720dc7ae 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -8,45 +8,11 @@ part of 'enum_converters.dart'; _SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() ..response = - _$enumDecode(_$SKPaymentTransactionStateWrapperEnumMap, json['response']) - ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) - ..discountPaymentMode = _$enumDecode( + $enumDecode(_$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = $enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = $enumDecode( _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); -Map _$SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], - 'discountPaymentMode': - _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], - }; - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - const _$SKPaymentTransactionStateWrapperEnumMap = { SKPaymentTransactionStateWrapper.purchasing: 0, SKPaymentTransactionStateWrapper.purchased: 1, diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 079e75078037..3decba2a9818 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -375,7 +375,7 @@ class SKError { /// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to /// initiate a payment. @immutable -@JsonSerializable() +@JsonSerializable(createToJson: true) class SKPaymentWrapper { /// Creates a new [SKPaymentWrapper] with the provided information. const SKPaymentWrapper( @@ -448,7 +448,6 @@ class SKPaymentWrapper { /// /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox /// testing. - @JsonKey(defaultValue: false) final bool simulatesAskToBuyInSandbox; @override diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 4d2b5e4b3d4b..8c0e64b2b9f6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -15,12 +15,6 @@ SKError _$SKErrorFromJson(Map json) => SKError( {}, ); -Map _$SKErrorToJson(SKError instance) => { - 'code': instance.code, - 'domain': instance.domain, - 'userInfo': instance.userInfo, - }; - SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) => SKPaymentWrapper( productIdentifier: json['productIdentifier'] as String? ?? '', applicationUsername: json['applicationUsername'] as String?, diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 01cd6db0dda1..885e9c075b48 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -3,10 +3,12 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; + import 'package:json_annotation/json_annotation.dart'; -import 'sk_product_wrapper.dart'; -import 'sk_payment_queue_wrapper.dart'; + import 'enum_converters.dart'; +import 'sk_payment_queue_wrapper.dart'; +import 'sk_product_wrapper.dart'; part 'sk_payment_transaction_wrappers.g.dart'; @@ -101,7 +103,7 @@ enum SKPaymentTransactionStateWrapper { /// /// Dart wrapper around StoreKit's /// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). -@JsonSerializable() +@JsonSerializable(createToJson: true) class SKPaymentTransactionWrapper { /// Creates a new [SKPaymentTransactionWrapper] with the provided information. SKPaymentTransactionWrapper({ diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index 485bf1932efa..c9079ff64a1e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -20,13 +20,6 @@ SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) => [], ); -Map _$SkProductResponseWrapperToJson( - SkProductResponseWrapper instance) => - { - 'products': instance.products, - 'invalidProductIdentifiers': instance.invalidProductIdentifiers, - }; - SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( Map json) => SKProductSubscriptionPeriodWrapper( @@ -35,13 +28,6 @@ SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( .fromJson(json['unit'] as int?), ); -Map _$SKProductSubscriptionPeriodWrapperToJson( - SKProductSubscriptionPeriodWrapper instance) => - { - 'numberOfUnits': instance.numberOfUnits, - 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), - }; - SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => SKProductDiscountWrapper( price: json['price'] as String? ?? '', @@ -58,17 +44,6 @@ SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => )), ); -Map _$SKProductDiscountWrapperToJson( - SKProductDiscountWrapper instance) => - { - 'price': instance.price, - 'priceLocale': instance.priceLocale, - 'numberOfPeriods': instance.numberOfPeriods, - 'paymentMode': const SKProductDiscountPaymentModeConverter() - .toJson(instance.paymentMode), - 'subscriptionPeriod': instance.subscriptionPeriod, - }; - SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( productIdentifier: json['productIdentifier'] as String? ?? '', localizedTitle: json['localizedTitle'] as String? ?? '', @@ -92,29 +67,9 @@ SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( Map.from(json['introductoryPrice'] as Map)), ); -Map _$SKProductWrapperToJson(SKProductWrapper instance) => - { - 'productIdentifier': instance.productIdentifier, - 'localizedTitle': instance.localizedTitle, - 'localizedDescription': instance.localizedDescription, - 'priceLocale': instance.priceLocale, - 'subscriptionGroupIdentifier': instance.subscriptionGroupIdentifier, - 'price': instance.price, - 'subscriptionPeriod': instance.subscriptionPeriod, - 'introductoryPrice': instance.introductoryPrice, - }; - SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) => SKPriceLocaleWrapper( currencySymbol: json['currencySymbol'] as String? ?? '', currencyCode: json['currencyCode'] as String? ?? '', countryCode: json['countryCode'] as String? ?? '', ); - -Map _$SKPriceLocaleWrapperToJson( - SKPriceLocaleWrapper instance) => - { - 'currencySymbol': instance.currencySymbol, - 'currencyCode': instance.currencyCode, - 'countryCode': instance.countryCode, - }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index 934fdea355e3..bcdae3206c00 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -12,7 +12,7 @@ part 'sk_storefront_wrapper.g.dart'; /// /// Dart wrapper around StoreKit's /// [SKStorefront](https://developer.apple.com/documentation/storekit/skstorefront?language=objc). -@JsonSerializable() +@JsonSerializable(createToJson: true) class SKStorefrontWrapper { /// Creates a new [SKStorefrontWrapper] with the provided information. SKStorefrontWrapper({ diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index fdd769e90674..b40ad1da149e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+5 +version: 0.1.4 environment: sdk: ">=2.14.0 <3.0.0" @@ -20,12 +20,12 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.1.0 - json_annotation: ^4.0.1 + json_annotation: ^4.3.0 meta: ^1.3.0 dev_dependencies: build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^5.0.2 + json_serializable: ^6.0.0 test: ^1.16.0 From 42364e49fc25653768f4f659564215d247d66f81 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 27 Oct 2021 09:46:27 -0700 Subject: [PATCH 120/259] Implement Android WebView api with pigeon (Java portion) (#4441) --- .../android/build.gradle | 4 + .../DownloadListenerHostApiImpl.java | 44 + .../webviewflutter/FlutterWebViewClient.java | 2 +- .../GeneratedAndroidWebView.java | 1909 +++++++++++++++++ .../webviewflutter/InputAwareWebView.java | 2 +- .../webviewflutter/InstanceManager.java | 39 + .../webviewflutter/JavaScriptChannel.java | 2 +- .../JavaScriptChannelHostApiImpl.java | 61 + .../WebChromeClientHostApiImpl.java | 92 + .../WebSettingsHostApiImpl.java | 101 + .../WebViewClientHostApiImpl.java | 239 +++ .../webviewflutter/WebViewHostApiImpl.java | 233 ++ .../java/android/util/LongSparseArray.java | 29 + .../webviewflutter/DownloadListenerTest.java | 65 + .../webviewflutter/JavaScriptChannelTest.java | 60 + .../webviewflutter/WebChromeClientTest.java | 67 + .../webviewflutter/WebSettingsTest.java | 103 + .../webviewflutter/WebViewClientTest.java | 77 + .../plugins/webviewflutter/WebViewTest.java | 173 ++ .../generatePigeons.sh | 4 +- 20 files changed, 3302 insertions(+), 4 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index 4a164317c60f..ef1485a765d0 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -42,6 +42,10 @@ android { testImplementation 'androidx.test:core:1.3.0' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java new file mode 100644 index 000000000000..202be87d7d1e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; + +class DownloadListenerHostApiImpl implements GeneratedAndroidWebView.DownloadListenerHostApi { + private final InstanceManager instanceManager; + private final DownloadListenerCreator downloadListenerCreator; + private final GeneratedAndroidWebView.DownloadListenerFlutterApi downloadListenerFlutterApi; + + static class DownloadListenerCreator { + DownloadListener createDownloadListener( + Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { + return (url, userAgent, contentDisposition, mimetype, contentLength) -> + downloadListenerFlutterApi.onDownloadStart( + instanceId, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + } + } + + DownloadListenerHostApiImpl( + InstanceManager instanceManager, + DownloadListenerCreator downloadListenerCreator, + DownloadListenerFlutterApi downloadListenerFlutterApi) { + this.instanceManager = instanceManager; + this.downloadListenerCreator = downloadListenerCreator; + this.downloadListenerFlutterApi = downloadListenerFlutterApi; + } + + @Override + public void create(Long instanceId) { + final DownloadListener downloadListener = + downloadListenerCreator.createDownloadListener(instanceId, downloadListenerFlutterApi); + instanceManager.addInstance(downloadListener, instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 260ef8e8b15d..a86d3e8a4b63 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -104,7 +104,7 @@ boolean shouldOverrideUrlLoading(WebView view, String url) { return false; } // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). + // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). // On these devices we cannot tell whether the navigation is targeted to the main frame or not. // We proceed assuming that the navigation is targeted to the main frame. If the page had any // frames they will be loaded in the main frame instead. 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 new file mode 100644 index 000000000000..ba2b9b1ac481 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -0,0 +1,1909 @@ +// Copyright 2013 The Flutter 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.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.webviewflutter; + +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class GeneratedAndroidWebView { + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class WebResourceRequestData { + private String url; + + public String getUrl() { + return url; + } + + public void setUrl(String setterArg) { + this.url = setterArg; + } + + private Boolean isForMainFrame; + + public Boolean getIsForMainFrame() { + return isForMainFrame; + } + + public void setIsForMainFrame(Boolean setterArg) { + this.isForMainFrame = setterArg; + } + + private Boolean isRedirect; + + public Boolean getIsRedirect() { + return isRedirect; + } + + public void setIsRedirect(Boolean setterArg) { + this.isRedirect = setterArg; + } + + private Boolean hasGesture; + + public Boolean getHasGesture() { + return hasGesture; + } + + public void setHasGesture(Boolean setterArg) { + this.hasGesture = setterArg; + } + + private String method; + + public String getMethod() { + return method; + } + + public void setMethod(String setterArg) { + this.method = setterArg; + } + + private Map requestHeaders; + + public Map getRequestHeaders() { + return requestHeaders; + } + + public void setRequestHeaders(Map setterArg) { + this.requestHeaders = setterArg; + } + + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("url", url); + toMapResult.put("isForMainFrame", isForMainFrame); + toMapResult.put("isRedirect", isRedirect); + toMapResult.put("hasGesture", hasGesture); + toMapResult.put("method", method); + toMapResult.put("requestHeaders", requestHeaders); + return toMapResult; + } + + static WebResourceRequestData fromMap(Map map) { + WebResourceRequestData fromMapResult = new WebResourceRequestData(); + Object url = map.get("url"); + fromMapResult.url = (String) url; + Object isForMainFrame = map.get("isForMainFrame"); + fromMapResult.isForMainFrame = (Boolean) isForMainFrame; + Object isRedirect = map.get("isRedirect"); + fromMapResult.isRedirect = (Boolean) isRedirect; + Object hasGesture = map.get("hasGesture"); + fromMapResult.hasGesture = (Boolean) hasGesture; + Object method = map.get("method"); + fromMapResult.method = (String) method; + Object requestHeaders = map.get("requestHeaders"); + fromMapResult.requestHeaders = (Map) requestHeaders; + return fromMapResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class WebResourceErrorData { + private Long errorCode; + + public Long getErrorCode() { + return errorCode; + } + + public void setErrorCode(Long setterArg) { + this.errorCode = setterArg; + } + + private String description; + + public String getDescription() { + return description; + } + + public void setDescription(String setterArg) { + this.description = setterArg; + } + + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("errorCode", errorCode); + toMapResult.put("description", description); + return toMapResult; + } + + static WebResourceErrorData fromMap(Map map) { + WebResourceErrorData fromMapResult = new WebResourceErrorData(); + Object errorCode = map.get("errorCode"); + fromMapResult.errorCode = + (errorCode == null) + ? null + : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode); + Object description = map.get("description"); + fromMapResult.description = (String) description; + return fromMapResult; + } + } + + public interface Result { + void success(T result); + + void error(Throwable error); + } + + private static class WebViewHostApiCodec extends StandardMessageCodec { + public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); + + private WebViewHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebViewHostApi { + void create(Long instanceId, Boolean useHybridComposition); + + void dispose(Long instanceId); + + void loadUrl(Long instanceId, String url, Map headers); + + String getUrl(Long instanceId); + + Boolean canGoBack(Long instanceId); + + Boolean canGoForward(Long instanceId); + + void goBack(Long instanceId); + + void goForward(Long instanceId); + + void reload(Long instanceId); + + void clearCache(Long instanceId, Boolean includeDiskFiles); + + void evaluateJavascript(Long instanceId, String javascriptString, Result result); + + String getTitle(Long instanceId); + + void scrollTo(Long instanceId, Long x, Long y); + + void scrollBy(Long instanceId, Long x, Long y); + + Long getScrollX(Long instanceId); + + Long getScrollY(Long instanceId); + + void setWebContentsDebuggingEnabled(Boolean enabled); + + void setWebViewClient(Long instanceId, Long webViewClientInstanceId); + + void addJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); + + void removeJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); + + void setDownloadListener(Long instanceId, Long listenerInstanceId); + + void setWebChromeClient(Long instanceId, Long clientInstanceId); + + /** The codec used by WebViewHostApi. */ + static MessageCodec getCodec() { + return WebViewHostApiCodec.INSTANCE; + } + + /** Sets up an instance of `WebViewHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.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."); + } + Boolean useHybridCompositionArg = (Boolean) args.get(1); + if (useHybridCompositionArg == null) { + throw new NullPointerException("useHybridCompositionArg unexpectedly null."); + } + api.create(instanceIdArg.longValue(), useHybridCompositionArg); + 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.WebViewHostApi.dispose", 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.dispose(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.WebViewHostApi.loadUrl", 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."); + } + String urlArg = (String) args.get(1); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + Map headersArg = (Map) args.get(2); + if (headersArg == null) { + throw new NullPointerException("headersArg unexpectedly null."); + } + api.loadUrl(instanceIdArg.longValue(), urlArg, headersArg); + 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.WebViewHostApi.getUrl", 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."); + } + String output = api.getUrl(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.canGoBack", 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."); + } + Boolean output = api.canGoBack(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.canGoForward", 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."); + } + Boolean output = api.canGoForward(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.goBack", 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.goBack(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.WebViewHostApi.goForward", 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.goForward(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.WebViewHostApi.reload", 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.reload(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.WebViewHostApi.clearCache", 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."); + } + Boolean includeDiskFilesArg = (Boolean) args.get(1); + if (includeDiskFilesArg == null) { + throw new NullPointerException("includeDiskFilesArg unexpectedly null."); + } + api.clearCache(instanceIdArg.longValue(), includeDiskFilesArg); + 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.WebViewHostApi.evaluateJavascript", + 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."); + } + String javascriptStringArg = (String) args.get(1); + if (javascriptStringArg == null) { + throw new NullPointerException("javascriptStringArg unexpectedly null."); + } + Result resultCallback = + new Result() { + public void success(String result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.evaluateJavascript( + instanceIdArg.longValue(), javascriptStringArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.getTitle", 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."); + } + String output = api.getTitle(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.scrollTo", 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."); + } + Number xArg = (Number) args.get(1); + if (xArg == null) { + throw new NullPointerException("xArg unexpectedly null."); + } + Number yArg = (Number) args.get(2); + if (yArg == null) { + throw new NullPointerException("yArg unexpectedly null."); + } + api.scrollTo(instanceIdArg.longValue(), xArg.longValue(), yArg.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.WebViewHostApi.scrollBy", 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."); + } + Number xArg = (Number) args.get(1); + if (xArg == null) { + throw new NullPointerException("xArg unexpectedly null."); + } + Number yArg = (Number) args.get(2); + if (yArg == null) { + throw new NullPointerException("yArg unexpectedly null."); + } + api.scrollBy(instanceIdArg.longValue(), xArg.longValue(), yArg.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.WebViewHostApi.getScrollX", 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."); + } + Long output = api.getScrollX(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.getScrollY", 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."); + } + Long output = api.getScrollY(instanceIdArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean enabledArg = (Boolean) args.get(0); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setWebContentsDebuggingEnabled(enabledArg); + 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.WebViewHostApi.setWebViewClient", 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."); + } + Number webViewClientInstanceIdArg = (Number) args.get(1); + if (webViewClientInstanceIdArg == null) { + throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); + } + api.setWebViewClient( + instanceIdArg.longValue(), webViewClientInstanceIdArg.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.WebViewHostApi.addJavaScriptChannel", + 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."); + } + Number javaScriptChannelInstanceIdArg = (Number) args.get(1); + if (javaScriptChannelInstanceIdArg == null) { + throw new NullPointerException( + "javaScriptChannelInstanceIdArg unexpectedly null."); + } + api.addJavaScriptChannel( + instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.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.WebViewHostApi.removeJavaScriptChannel", + 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."); + } + Number javaScriptChannelInstanceIdArg = (Number) args.get(1); + if (javaScriptChannelInstanceIdArg == null) { + throw new NullPointerException( + "javaScriptChannelInstanceIdArg unexpectedly null."); + } + api.removeJavaScriptChannel( + instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.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.WebViewHostApi.setDownloadListener", + 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."); + } + Number listenerInstanceIdArg = (Number) args.get(1); + if (listenerInstanceIdArg == null) { + throw new NullPointerException("listenerInstanceIdArg unexpectedly null."); + } + api.setDownloadListener( + instanceIdArg.longValue(), listenerInstanceIdArg.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.WebViewHostApi.setWebChromeClient", + 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."); + } + Number clientInstanceIdArg = (Number) args.get(1); + if (clientInstanceIdArg == null) { + throw new NullPointerException("clientInstanceIdArg unexpectedly null."); + } + api.setWebChromeClient( + instanceIdArg.longValue(), clientInstanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class WebSettingsHostApiCodec extends StandardMessageCodec { + public static final WebSettingsHostApiCodec INSTANCE = new WebSettingsHostApiCodec(); + + private WebSettingsHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebSettingsHostApi { + void create(Long instanceId, Long webViewInstanceId); + + void dispose(Long instanceId); + + void setDomStorageEnabled(Long instanceId, Boolean flag); + + void setJavaScriptCanOpenWindowsAutomatically(Long instanceId, Boolean flag); + + void setSupportMultipleWindows(Long instanceId, Boolean support); + + void setJavaScriptEnabled(Long instanceId, Boolean flag); + + void setUserAgentString(Long instanceId, String userAgentString); + + void setMediaPlaybackRequiresUserGesture(Long instanceId, Boolean require); + + void setSupportZoom(Long instanceId, Boolean support); + + void setLoadWithOverviewMode(Long instanceId, Boolean overview); + + void setUseWideViewPort(Long instanceId, Boolean use); + + void setDisplayZoomControls(Long instanceId, Boolean enabled); + + void setBuiltInZoomControls(Long instanceId, Boolean enabled); + + /** The codec used by WebSettingsHostApi. */ + static MessageCodec getCodec() { + return WebSettingsHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `WebSettingsHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebSettingsHostApi.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."); + } + Number webViewInstanceIdArg = (Number) args.get(1); + if (webViewInstanceIdArg == null) { + throw new NullPointerException("webViewInstanceIdArg unexpectedly null."); + } + api.create(instanceIdArg.longValue(), webViewInstanceIdArg.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.WebSettingsHostApi.dispose", 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.dispose(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.WebSettingsHostApi.setDomStorageEnabled", + 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."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setDomStorageEnabled(instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically", + 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."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setJavaScriptCanOpenWindowsAutomatically(instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setSupportMultipleWindows", + 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."); + } + Boolean supportArg = (Boolean) args.get(1); + if (supportArg == null) { + throw new NullPointerException("supportArg unexpectedly null."); + } + api.setSupportMultipleWindows(instanceIdArg.longValue(), supportArg); + 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.WebSettingsHostApi.setJavaScriptEnabled", + 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."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setJavaScriptEnabled(instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setUserAgentString", + 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."); + } + String userAgentStringArg = (String) args.get(1); + if (userAgentStringArg == null) { + throw new NullPointerException("userAgentStringArg unexpectedly null."); + } + api.setUserAgentString(instanceIdArg.longValue(), userAgentStringArg); + 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.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture", + 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."); + } + Boolean requireArg = (Boolean) args.get(1); + if (requireArg == null) { + throw new NullPointerException("requireArg unexpectedly null."); + } + api.setMediaPlaybackRequiresUserGesture(instanceIdArg.longValue(), requireArg); + 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.WebSettingsHostApi.setSupportZoom", + 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."); + } + Boolean supportArg = (Boolean) args.get(1); + if (supportArg == null) { + throw new NullPointerException("supportArg unexpectedly null."); + } + api.setSupportZoom(instanceIdArg.longValue(), supportArg); + 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.WebSettingsHostApi.setLoadWithOverviewMode", + 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."); + } + Boolean overviewArg = (Boolean) args.get(1); + if (overviewArg == null) { + throw new NullPointerException("overviewArg unexpectedly null."); + } + api.setLoadWithOverviewMode(instanceIdArg.longValue(), overviewArg); + 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.WebSettingsHostApi.setUseWideViewPort", + 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."); + } + Boolean useArg = (Boolean) args.get(1); + if (useArg == null) { + throw new NullPointerException("useArg unexpectedly null."); + } + api.setUseWideViewPort(instanceIdArg.longValue(), useArg); + 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.WebSettingsHostApi.setDisplayZoomControls", + 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."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setDisplayZoomControls(instanceIdArg.longValue(), enabledArg); + 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.WebSettingsHostApi.setBuiltInZoomControls", + 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."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setBuiltInZoomControls(instanceIdArg.longValue(), enabledArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class JavaScriptChannelHostApiCodec extends StandardMessageCodec { + public static final JavaScriptChannelHostApiCodec INSTANCE = + new JavaScriptChannelHostApiCodec(); + + private JavaScriptChannelHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface JavaScriptChannelHostApi { + void create(Long instanceId, String channelName); + + void dispose(Long instanceId); + + /** The codec used by JavaScriptChannelHostApi. */ + static MessageCodec getCodec() { + return JavaScriptChannelHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `JavaScriptChannelHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelHostApi.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."); + } + String channelNameArg = (String) args.get(1); + if (channelNameArg == null) { + throw new NullPointerException("channelNameArg unexpectedly null."); + } + api.create(instanceIdArg.longValue(), channelNameArg); + 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.JavaScriptChannelHostApi.dispose", 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.dispose(instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { + public static final JavaScriptChannelFlutterApiCodec INSTANCE = + new JavaScriptChannelFlutterApiCodec(); + + private JavaScriptChannelFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class JavaScriptChannelFlutterApi { + private final BinaryMessenger binaryMessenger; + + public JavaScriptChannelFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return JavaScriptChannelFlutterApiCodec.INSTANCE; + } + + public void postMessage(Long instanceIdArg, String messageArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, messageArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class WebViewClientHostApiCodec extends StandardMessageCodec { + public static final WebViewClientHostApiCodec INSTANCE = new WebViewClientHostApiCodec(); + + private WebViewClientHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebViewClientHostApi { + void create(Long instanceId, Boolean shouldOverrideUrlLoading); + + void dispose(Long instanceId); + + /** The codec used by WebViewClientHostApi. */ + static MessageCodec getCodec() { + return WebViewClientHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `WebViewClientHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewClientHostApi.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."); + } + Boolean shouldOverrideUrlLoadingArg = (Boolean) args.get(1); + if (shouldOverrideUrlLoadingArg == null) { + throw new NullPointerException( + "shouldOverrideUrlLoadingArg unexpectedly null."); + } + api.create(instanceIdArg.longValue(), shouldOverrideUrlLoadingArg); + 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.WebViewClientHostApi.dispose", 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.dispose(instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class WebViewClientFlutterApiCodec extends StandardMessageCodec { + public static final WebViewClientFlutterApiCodec INSTANCE = new WebViewClientFlutterApiCodec(); + + private WebViewClientFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return WebResourceErrorData.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return WebResourceRequestData.fromMap((Map) readValue(buffer)); + + case (byte) 130: + return WebResourceRequestData.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof WebResourceErrorData) { + stream.write(128); + writeValue(stream, ((WebResourceErrorData) value).toMap()); + } else if (value instanceof WebResourceRequestData) { + stream.write(129); + writeValue(stream, ((WebResourceRequestData) value).toMap()); + } else if (value instanceof WebResourceRequestData) { + stream.write(130); + writeValue(stream, ((WebResourceRequestData) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class WebViewClientFlutterApi { + private final BinaryMessenger binaryMessenger; + + public WebViewClientFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return WebViewClientFlutterApiCodec.INSTANCE; + } + + public void onPageStarted( + Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void onPageFinished( + Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void onReceivedRequestError( + Long instanceIdArg, + Long webViewInstanceIdArg, + WebResourceRequestData requestArg, + WebResourceErrorData errorArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, webViewInstanceIdArg, requestArg, errorArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void onReceivedError( + Long instanceIdArg, + Long webViewInstanceIdArg, + Long errorCodeArg, + String descriptionArg, + String failingUrlArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList( + instanceIdArg, + webViewInstanceIdArg, + errorCodeArg, + descriptionArg, + failingUrlArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void requestLoading( + Long instanceIdArg, + Long webViewInstanceIdArg, + WebResourceRequestData requestArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, requestArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void urlLoading( + Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class DownloadListenerHostApiCodec extends StandardMessageCodec { + public static final DownloadListenerHostApiCodec INSTANCE = new DownloadListenerHostApiCodec(); + + private DownloadListenerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface DownloadListenerHostApi { + void create(Long instanceId); + + void dispose(Long instanceId); + + /** The codec used by DownloadListenerHostApi. */ + static MessageCodec getCodec() { + return DownloadListenerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `DownloadListenerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.DownloadListenerHostApi.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.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.DownloadListenerHostApi.dispose", 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.dispose(instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class DownloadListenerFlutterApiCodec extends StandardMessageCodec { + public static final DownloadListenerFlutterApiCodec INSTANCE = + new DownloadListenerFlutterApiCodec(); + + private DownloadListenerFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class DownloadListenerFlutterApi { + private final BinaryMessenger binaryMessenger; + + public DownloadListenerFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return DownloadListenerFlutterApiCodec.INSTANCE; + } + + public void onDownloadStart( + Long instanceIdArg, + String urlArg, + String userAgentArg, + String contentDispositionArg, + String mimetypeArg, + Long contentLengthArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList( + instanceIdArg, + urlArg, + userAgentArg, + contentDispositionArg, + mimetypeArg, + contentLengthArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class WebChromeClientHostApiCodec extends StandardMessageCodec { + public static final WebChromeClientHostApiCodec INSTANCE = new WebChromeClientHostApiCodec(); + + private WebChromeClientHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebChromeClientHostApi { + void create(Long instanceId, Long webViewClientInstanceId); + + void dispose(Long instanceId); + + /** The codec used by WebChromeClientHostApi. */ + static MessageCodec getCodec() { + return WebChromeClientHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `WebChromeClientHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebChromeClientHostApi.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."); + } + Number webViewClientInstanceIdArg = (Number) args.get(1); + if (webViewClientInstanceIdArg == null) { + throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); + } + api.create(instanceIdArg.longValue(), webViewClientInstanceIdArg.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.WebChromeClientHostApi.dispose", 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.dispose(instanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { + public static final WebChromeClientFlutterApiCodec INSTANCE = + new WebChromeClientFlutterApiCodec(); + + private WebChromeClientFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class WebChromeClientFlutterApi { + private final BinaryMessenger binaryMessenger; + + public WebChromeClientFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return WebChromeClientFlutterApiCodec.INSTANCE; + } + + public void onProgressChanged( + Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, progressArg)), + channelReply -> { + callback.reply(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); + return errorMap; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java index 51b2a3809fff..0a4cb3effd29 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -25,7 +25,7 @@ * *

See also {@link ThreadedInputConnectionProxyAdapterView}. */ -final class InputAwareWebView extends WebView { +class InputAwareWebView extends WebView { private static final String TAG = "InputAwareWebView"; private View threadedInputConnectionProxyView; private ThreadedInputConnectionProxyAdapterView proxyAdapterView; diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java new file mode 100644 index 000000000000..bfa7d6f17345 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.util.LongSparseArray; +import java.util.HashMap; +import java.util.Map; + +class InstanceManager { + private final LongSparseArray instanceIdsToInstances = new LongSparseArray<>(); + private final Map instancesToInstanceIds = new HashMap<>(); + + /** Add a new instance with instanceId. */ + void addInstance(Object instance, long instanceId) { + instancesToInstanceIds.put(instance, instanceId); + instanceIdsToInstances.append(instanceId, instance); + } + + /** Remove the instance from the manager. */ + void removeInstance(long instanceId) { + final Object instance = instanceIdsToInstances.get(instanceId); + if (instance != null) { + instanceIdsToInstances.remove(instanceId); + instancesToInstanceIds.remove(instance); + } + } + + /** Retrieve the Object paired with instanceId. */ + Object getInstance(long instanceId) { + return instanceIdsToInstances.get(instanceId); + } + + /** Retrieve the instanceId paired with instance. */ + Long getInstanceId(Object instance) { + return instancesToInstanceIds.get(instance); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index 4d596351b3d0..2f987c0f86b3 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -19,7 +19,7 @@ */ class JavaScriptChannel { private final MethodChannel methodChannel; - private final String javaScriptChannelName; + final String javaScriptChannelName; private final Handler platformThreadHandler; /** diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java new file mode 100644 index 000000000000..2d42d952955c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.Looper; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; + +class JavaScriptChannelHostApiImpl implements GeneratedAndroidWebView.JavaScriptChannelHostApi { + private final InstanceManager instanceManager; + private final JavaScriptChannelCreator javaScriptChannelCreator; + private final JavaScriptChannelFlutterApi javaScriptChannelFlutterApi; + private final Handler platformThreadHandler; + + static class JavaScriptChannelCreator { + JavaScriptChannel createJavaScriptChannel( + Long instanceId, + JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + String channelName, + Handler platformThreadHandler) { + return new JavaScriptChannel(null, channelName, platformThreadHandler) { + @Override + public void postMessage(String message) { + final Runnable postMessageRunnable = + () -> javaScriptChannelFlutterApi.postMessage(instanceId, message, reply -> {}); + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } + } + }; + } + } + + JavaScriptChannelHostApiImpl( + InstanceManager instanceManager, + JavaScriptChannelCreator javaScriptChannelCreator, + JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + Handler platformThreadHandler) { + this.instanceManager = instanceManager; + this.javaScriptChannelCreator = javaScriptChannelCreator; + this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi; + this.platformThreadHandler = platformThreadHandler; + } + + @Override + public void create(Long instanceId, String channelName) { + final JavaScriptChannel javaScriptChannel = + javaScriptChannelCreator.createJavaScriptChannel( + instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler); + instanceManager.addInstance(javaScriptChannel, instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java new file mode 100644 index 000000000000..32f8fcbdeed9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.os.Message; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; + +class WebChromeClientHostApiImpl implements GeneratedAndroidWebView.WebChromeClientHostApi { + private final InstanceManager instanceManager; + private final WebChromeClientCreator webChromeClientCreator; + private final WebChromeClientFlutterApi webChromeClientFlutterApi; + + static class WebChromeClientCreator { + WebChromeClient createWebChromeClient( + Long instanceId, + InstanceManager instanceManager, + WebViewClient webViewClient, + WebChromeClientFlutterApi webChromeClientFlutterApi) { + return new WebChromeClient() { + // Verifies that a url opened by `Window.open` has a secure url. + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient newWindowWebViewClient = + new WebViewClient() { + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webViewClient.shouldOverrideUrlLoading(view, request); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webViewClient.shouldOverrideUrlLoading(view, url); + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(newWindowWebViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + webChromeClientFlutterApi.onProgressChanged( + instanceId, instanceManager.getInstanceId(view), (long) progress, reply -> {}); + } + }; + } + } + + WebChromeClientHostApiImpl( + InstanceManager instanceManager, + WebChromeClientCreator webChromeClientCreator, + WebChromeClientFlutterApi webChromeClientFlutterApi) { + this.instanceManager = instanceManager; + this.webChromeClientCreator = webChromeClientCreator; + this.webChromeClientFlutterApi = webChromeClientFlutterApi; + } + + @Override + public void create(Long instanceId, Long webViewClientInstanceId) { + final WebViewClient webViewClient = + (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); + final WebChromeClient webChromeClient = + webChromeClientCreator.createWebChromeClient( + instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); + instanceManager.addInstance(webChromeClient, instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java new file mode 100644 index 000000000000..e70a867c23ff --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebSettings; +import android.webkit.WebView; + +class WebSettingsHostApiImpl implements GeneratedAndroidWebView.WebSettingsHostApi { + private final InstanceManager instanceManager; + private final WebSettingsCreator webSettingsCreator; + + static class WebSettingsCreator { + WebSettings createWebSettings(WebView webView) { + return webView.getSettings(); + } + } + + WebSettingsHostApiImpl(InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) { + this.instanceManager = instanceManager; + this.webSettingsCreator = webSettingsCreator; + } + + @Override + public void create(Long instanceId, Long webViewInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(webViewInstanceId); + instanceManager.addInstance(webSettingsCreator.createWebSettings(webView), instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } + + @Override + public void setDomStorageEnabled(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setDomStorageEnabled(flag); + } + + @Override + public void setJavaScriptCanOpenWindowsAutomatically(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setJavaScriptCanOpenWindowsAutomatically(flag); + } + + @Override + public void setSupportMultipleWindows(Long instanceId, Boolean support) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setSupportMultipleWindows(support); + } + + @Override + public void setJavaScriptEnabled(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setJavaScriptEnabled(flag); + } + + @Override + public void setUserAgentString(Long instanceId, String userAgentString) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setUserAgentString(userAgentString); + } + + @Override + public void setMediaPlaybackRequiresUserGesture(Long instanceId, Boolean require) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setMediaPlaybackRequiresUserGesture(require); + } + + @Override + public void setSupportZoom(Long instanceId, Boolean support) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setSupportZoom(support); + } + + @Override + public void setLoadWithOverviewMode(Long instanceId, Boolean overview) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setLoadWithOverviewMode(overview); + } + + @Override + public void setUseWideViewPort(Long instanceId, Boolean use) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setUseWideViewPort(use); + } + + @Override + public void setDisplayZoomControls(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setDisplayZoomControls(enabled); + } + + @Override + public void setBuiltInZoomControls(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setBuiltInZoomControls(enabled); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java new file mode 100644 index 000000000000..4d17eb129db8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.view.KeyEvent; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewClientCompat; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; + +class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi { + private final InstanceManager instanceManager; + private final WebViewClientCreator webViewClientCreator; + private final WebViewClientFlutterApi 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; + } + + @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; + } + + @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()); + 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; + } + + static class WebViewClientCreator { + WebViewClient createWebViewClient( + Long instanceId, + InstanceManager instanceManager, + Boolean shouldOverrideUrlLoading, + WebViewClientFlutterApi webViewClientFlutterApi) { + // WebViewClientCompat is used to get + // shouldOverrideUrlLoading(WebView view, WebResourceRequest request) + // invoked by the webview on older Android devices, without it pages that use iframes will + // be broken when a navigationDelegate is set on Android version earlier than N. + // + // However, this if statement attempts to avoid using WebViewClientCompat on versions >= N due + // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see + // https://github.com/flutter/flutter/issues/29446. + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return new WebViewClient() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + webViewClientFlutterApi.onPageStarted( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + + @Override + public void onPageFinished(WebView view, String url) { + webViewClientFlutterApi.onPageFinished( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + + @Override + public void onReceivedError( + WebView view, WebResourceRequest request, WebResourceError error) { + webViewClientFlutterApi.onReceivedRequestError( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + reply -> {}); + } + + @SuppressWarnings("deprecation") + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + webViewClientFlutterApi.onReceivedError( + instanceId, + instanceManager.getInstanceId(view), + (long) errorCode, + description, + failingUrl, + reply -> {}); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + webViewClientFlutterApi.requestLoading( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + reply -> {}); + return shouldOverrideUrlLoading; + } + + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webViewClientFlutterApi.urlLoading( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } else { + return new WebViewClientCompat() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + webViewClientFlutterApi.onPageStarted( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + + @Override + public void onPageFinished(WebView view, String url) { + webViewClientFlutterApi.onPageFinished( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + webViewClientFlutterApi.onReceivedRequestError( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + reply -> {}); + } + + @SuppressWarnings("deprecation") + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + webViewClientFlutterApi.onReceivedError( + instanceId, + instanceManager.getInstanceId(view), + (long) errorCode, + description, + failingUrl, + reply -> {}); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webViewClientFlutterApi.requestLoading( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + reply -> {}); + return shouldOverrideUrlLoading; + } + + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webViewClientFlutterApi.urlLoading( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } + } + } + + WebViewClientHostApiImpl( + InstanceManager instanceManager, + WebViewClientCreator webViewClientCreator, + WebViewClientFlutterApi webViewClientFlutterApi) { + this.instanceManager = instanceManager; + this.webViewClientCreator = webViewClientCreator; + this.webViewClientFlutterApi = webViewClientFlutterApi; + } + + @Override + public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { + final WebViewClient webViewClient = + webViewClientCreator.createWebViewClient( + instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); + instanceManager.addInstance(webViewClient, instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } +} 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 new file mode 100644 index 000000000000..35bdc608d6ff --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -0,0 +1,233 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import io.flutter.plugin.platform.PlatformView; +import java.util.Map; + +class WebViewHostApiImpl implements GeneratedAndroidWebView.WebViewHostApi { + private final InstanceManager instanceManager; + private final WebViewProxy webViewProxy; + private final Context context; + + static class WebViewProxy { + WebView createWebView(Context context) { + return new WebViewPlatformView(context); + } + + WebView createInputAwareWebView(Context context) { + return new InputAwareWebViewPlatformView(context, null); + } + + void setWebContentsDebuggingEnabled(boolean enabled) { + WebView.setWebContentsDebuggingEnabled(enabled); + } + } + + private static class WebViewPlatformView extends WebView implements PlatformView { + public WebViewPlatformView(Context context) { + super(context); + } + + @Override + public View getView() { + return this; + } + + @Override + public void dispose() { + destroy(); + } + } + + private static class InputAwareWebViewPlatformView extends InputAwareWebView + implements PlatformView { + InputAwareWebViewPlatformView(Context context, View containerView) { + super(context, containerView); + } + + @Override + public View getView() { + return this; + } + + @Override + public void onFlutterViewAttached(@NonNull View flutterView) { + setContainerView(flutterView); + } + + @Override + public void onFlutterViewDetached() { + setContainerView(null); + } + + @Override + public void dispose() { + dispose(); + destroy(); + } + + @Override + public void onInputConnectionLocked() { + lockInputConnection(); + } + + @Override + public void onInputConnectionUnlocked() { + unlockInputConnection(); + } + } + + WebViewHostApiImpl(InstanceManager instanceManager, WebViewProxy webViewProxy, Context context) { + this.instanceManager = instanceManager; + this.webViewProxy = webViewProxy; + this.context = context; + } + + @Override + public void create(Long instanceId, Boolean useHybridComposition) { + final WebView webView = + useHybridComposition + ? webViewProxy.createWebView(context) + : webViewProxy.createInputAwareWebView(context); + instanceManager.addInstance(webView, instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstance(instanceId); + } + + @Override + public void loadUrl(Long instanceId, String url, Map headers) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.loadUrl(url, headers); + } + + @Override + public String getUrl(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.getUrl(); + } + + @Override + public Boolean canGoBack(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.canGoBack(); + } + + @Override + public Boolean canGoForward(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.canGoForward(); + } + + @Override + public void goBack(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.goBack(); + } + + @Override + public void goForward(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.goForward(); + } + + @Override + public void reload(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.reload(); + } + + @Override + public void clearCache(Long instanceId, Boolean includeDiskFiles) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.clearCache(includeDiskFiles); + } + + @Override + public void evaluateJavascript( + Long instanceId, String javascriptString, GeneratedAndroidWebView.Result result) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.evaluateJavascript(javascriptString, result::success); + } + + @Override + public String getTitle(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.getTitle(); + } + + @Override + public void scrollTo(Long instanceId, Long x, Long y) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.scrollTo(x.intValue(), y.intValue()); + } + + @Override + public void scrollBy(Long instanceId, Long x, Long y) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.scrollBy(x.intValue(), y.intValue()); + } + + @Override + public Long getScrollX(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return (long) webView.getScrollX(); + } + + @Override + public Long getScrollY(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return (long) webView.getScrollY(); + } + + @Override + public void setWebContentsDebuggingEnabled(Boolean enabled) { + webViewProxy.setWebContentsDebuggingEnabled(enabled); + } + + @Override + public void setWebViewClient(Long instanceId, Long webViewClientInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setWebViewClient((WebViewClient) instanceManager.getInstance(webViewClientInstanceId)); + } + + @Override + public void addJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + final JavaScriptChannel javaScriptChannel = + (JavaScriptChannel) instanceManager.getInstance(javaScriptChannelInstanceId); + webView.addJavascriptInterface(javaScriptChannel, javaScriptChannel.javaScriptChannelName); + } + + @Override + public void removeJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + final JavaScriptChannel javaScriptChannel = + (JavaScriptChannel) instanceManager.getInstance(javaScriptChannelInstanceId); + webView.removeJavascriptInterface(javaScriptChannel.javaScriptChannelName); + } + + @Override + public void setDownloadListener(Long instanceId, Long listenerInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setDownloadListener((DownloadListener) instanceManager.getInstance(listenerInstanceId)); + } + + @Override + public void setWebChromeClient(Long instanceId, Long clientInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java new file mode 100644 index 000000000000..4a90e394e259 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package android.util; + +import java.util.HashMap; + +// Creates an implementation of LongSparseArray that can be used with unittests and the JVM. +// Typically android.util.LongSparseArray does nothing when not used with an Android environment. +public class LongSparseArray { + private final HashMap mHashMap; + + public LongSparseArray() { + mHashMap = new HashMap<>(); + } + + public void append(long key, E value) { + mHashMap.put(key, value); + } + + public E get(long key) { + return mHashMap.get(key); + } + + public void remove(long key) { + mHashMap.remove(key); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java new file mode 100644 index 000000000000..5ba073c7f418 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.DownloadListener; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; +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 DownloadListenerTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public DownloadListenerFlutterApi mockFlutterApi; + + InstanceManager testInstanceManager; + DownloadListenerHostApiImpl testHostApiImpl; + DownloadListener testDownloadListener; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + + final DownloadListenerCreator downloadListenerCreator = + new DownloadListenerCreator() { + @Override + DownloadListener createDownloadListener( + Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { + testDownloadListener = + super.createDownloadListener(instanceId, downloadListenerFlutterApi); + return testDownloadListener; + } + }; + + testHostApiImpl = + new DownloadListenerHostApiImpl( + testInstanceManager, downloadListenerCreator, mockFlutterApi); + testHostApiImpl.create(0L); + } + + @Test + public void postMessage() { + testDownloadListener.onDownloadStart( + "https://www.google.com", "userAgent", "contentDisposition", "mimetype", 54); + verify(mockFlutterApi) + .onDownloadStart( + eq(0L), + eq("https://www.google.com"), + eq("userAgent"), + eq("contentDisposition"), + eq("mimetype"), + eq(54L), + any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java new file mode 100644 index 000000000000..697ea0b70b90 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; +import io.flutter.plugins.webviewflutter.JavaScriptChannelHostApiImpl.JavaScriptChannelCreator; +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 JavaScriptChannelTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public GeneratedAndroidWebView.JavaScriptChannelFlutterApi mockFlutterApi; + + InstanceManager testInstanceManager; + JavaScriptChannelHostApiImpl testHostApiImpl; + JavaScriptChannel testJavaScriptChannel; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + + final JavaScriptChannelCreator javaScriptChannelCreator = + new JavaScriptChannelCreator() { + @Override + JavaScriptChannel createJavaScriptChannel( + Long instanceId, + JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + String channelName, + Handler platformThreadHandler) { + testJavaScriptChannel = + super.createJavaScriptChannel( + instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler); + return testJavaScriptChannel; + } + }; + + testHostApiImpl = + new JavaScriptChannelHostApiImpl( + testInstanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler()); + testHostApiImpl.create(0L, "aChannelName"); + } + + @Test + public void postMessage() { + testJavaScriptChannel.postMessage("A message post."); + verify(mockFlutterApi).postMessage(eq(0L), eq("A message post."), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java new file mode 100644 index 000000000000..5ab3ab10fe03 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator; +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 WebChromeClientTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebChromeClientFlutterApi mockFlutterApi; + + @Mock public WebView mockWebView; + + @Mock public WebViewClient mockWebViewClient; + + InstanceManager testInstanceManager; + WebChromeClientHostApiImpl testHostApiImpl; + WebChromeClient testWebChromeClient; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + testInstanceManager.addInstance(mockWebView, 0L); + testInstanceManager.addInstance(mockWebViewClient, 1L); + + final WebChromeClientCreator webChromeClientCreator = + new WebChromeClientCreator() { + @Override + WebChromeClient createWebChromeClient( + Long instanceId, + InstanceManager instanceManager, + WebViewClient webViewClient, + WebChromeClientFlutterApi webChromeClientFlutterApi) { + testWebChromeClient = + super.createWebChromeClient( + instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); + return testWebChromeClient; + } + }; + + testHostApiImpl = + new WebChromeClientHostApiImpl(testInstanceManager, webChromeClientCreator, mockFlutterApi); + testHostApiImpl.create(2L, 1L); + } + + @Test + public void onProgressChanged() { + testWebChromeClient.onProgressChanged(mockWebView, 23); + verify(mockFlutterApi).onProgressChanged(eq(2L), eq(0L), eq(23L), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java new file mode 100644 index 000000000000..8ef32ddcb4ca --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java @@ -0,0 +1,103 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebSettings; +import io.flutter.plugins.webviewflutter.WebSettingsHostApiImpl.WebSettingsCreator; +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 WebSettingsTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebSettings mockWebSettings; + + @Mock WebSettingsCreator mockWebSettingsCreator; + + InstanceManager testInstanceManager; + WebSettingsHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + when(mockWebSettingsCreator.createWebSettings(any())).thenReturn(mockWebSettings); + testHostApiImpl = new WebSettingsHostApiImpl(testInstanceManager, mockWebSettingsCreator); + testHostApiImpl.create(0L, 0L); + } + + @Test + public void setDomStorageEnabled() { + testHostApiImpl.setDomStorageEnabled(0L, true); + verify(mockWebSettings).setDomStorageEnabled(true); + } + + @Test + public void setJavaScriptCanOpenWindowsAutomatically() { + testHostApiImpl.setJavaScriptCanOpenWindowsAutomatically(0L, false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + } + + @Test + public void setSupportMultipleWindows() { + testHostApiImpl.setSupportMultipleWindows(0L, true); + verify(mockWebSettings).setSupportMultipleWindows(true); + } + + @Test + public void setJavaScriptEnabled() { + testHostApiImpl.setJavaScriptEnabled(0L, false); + verify(mockWebSettings).setJavaScriptEnabled(false); + } + + @Test + public void setUserAgentString() { + testHostApiImpl.setUserAgentString(0L, "hello"); + verify(mockWebSettings).setUserAgentString("hello"); + } + + @Test + public void setMediaPlaybackRequiresUserGesture() { + testHostApiImpl.setMediaPlaybackRequiresUserGesture(0L, false); + verify(mockWebSettings).setMediaPlaybackRequiresUserGesture(false); + } + + @Test + public void setSupportZoom() { + testHostApiImpl.setSupportZoom(0L, true); + verify(mockWebSettings).setSupportZoom(true); + } + + @Test + public void setLoadWithOverviewMode() { + testHostApiImpl.setLoadWithOverviewMode(0L, false); + verify(mockWebSettings).setLoadWithOverviewMode(false); + } + + @Test + public void setUseWideViewPort() { + testHostApiImpl.setUseWideViewPort(0L, true); + verify(mockWebSettings).setUseWideViewPort(true); + } + + @Test + public void setDisplayZoomControls() { + testHostApiImpl.setDisplayZoomControls(0L, false); + verify(mockWebSettings).setDisplayZoomControls(false); + } + + @Test + public void setBuiltInZoomControls() { + testHostApiImpl.setBuiltInZoomControls(0L, true); + verify(mockWebSettings).setBuiltInZoomControls(true); + } +} 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 new file mode 100644 index 000000000000..f6d2054564e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.WebView; +import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCreator; +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 WebViewClientTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebViewClientFlutterApi mockFlutterApi; + + @Mock public WebView mockWebView; + + InstanceManager testInstanceManager; + WebViewClientHostApiImpl testHostApiImpl; + WebViewClient testWebViewClient; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + testInstanceManager.addInstance(mockWebView, 0L); + + final WebViewClientCreator webViewClientCreator = + new WebViewClientCreator() { + @Override + WebViewClient createWebViewClient( + Long instanceId, + InstanceManager instanceManager, + Boolean shouldOverrideUrlLoading, + WebViewClientFlutterApi webViewClientFlutterApi) { + testWebViewClient = + super.createWebViewClient( + instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); + return testWebViewClient; + } + }; + + testHostApiImpl = + new WebViewClientHostApiImpl(testInstanceManager, webViewClientCreator, mockFlutterApi); + testHostApiImpl.create(1L, true); + } + + @Test + public void onPageStarted() { + testWebViewClient.onPageStarted(mockWebView, "https://www.google.com", null); + verify(mockFlutterApi).onPageStarted(eq(1L), eq(0L), eq("https://www.google.com"), any()); + } + + @Test + public void onReceivedError() { + testWebViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com"); + verify(mockFlutterApi) + .onReceivedError( + eq(1L), eq(0L), eq(32L), eq("description"), eq("https://www.google.com"), any()); + } + + @Test + public void urlLoading() { + testWebViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com"); + verify(mockFlutterApi).urlLoading(eq(1L), eq(0L), eq("https://www.google.com"), any()); + } +} 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 131a5a3eb53a..b914ce913e76 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 @@ -5,11 +5,45 @@ package io.flutter.plugins.webviewflutter; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.Context; +import android.webkit.DownloadListener; +import android.webkit.ValueCallback; +import android.webkit.WebView; import android.webkit.WebViewClient; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; public class WebViewTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebView mockWebView; + + @Mock WebViewHostApiImpl.WebViewProxy mockWebViewProxy; + + @Mock Context mockContext; + + InstanceManager testInstanceManager; + WebViewHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView); + testHostApiImpl = new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext); + testHostApiImpl.create(0L, true); + } + @Test public void errorCodes() { assertEquals( @@ -46,4 +80,143 @@ public void errorCodes() { FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), "unsupportedScheme"); } + + @Test + public void loadUrl() { + testHostApiImpl.loadUrl(0L, "https://www.google.com", new HashMap<>()); + verify(mockWebView).loadUrl("https://www.google.com", new HashMap<>()); + } + + @Test + public void getUrl() { + when(mockWebView.getUrl()).thenReturn("https://www.google.com"); + assertEquals(testHostApiImpl.getUrl(0L), "https://www.google.com"); + } + + @Test + public void canGoBack() { + when(mockWebView.canGoBack()).thenReturn(true); + assertEquals(testHostApiImpl.canGoBack(0L), true); + } + + @Test + public void canGoForward() { + when(mockWebView.canGoForward()).thenReturn(false); + assertEquals(testHostApiImpl.canGoForward(0L), false); + } + + @Test + public void goBack() { + testHostApiImpl.goBack(0L); + verify(mockWebView).goBack(); + } + + @Test + public void goForward() { + testHostApiImpl.goForward(0L); + verify(mockWebView).goForward(); + } + + @Test + public void reload() { + testHostApiImpl.reload(0L); + verify(mockWebView).reload(); + } + + @Test + public void clearCache() { + testHostApiImpl.clearCache(0L, false); + verify(mockWebView).clearCache(false); + } + + @Test + public void evaluateJavaScript() { + final String[] successValue = new String[1]; + testHostApiImpl.evaluateJavascript( + 0L, + "2 + 2", + new GeneratedAndroidWebView.Result() { + @Override + public void success(String result) { + successValue[0] = result; + } + + @Override + public void error(Throwable error) {} + }); + + @SuppressWarnings("unchecked") + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(ValueCallback.class); + verify(mockWebView).evaluateJavascript(eq("2 + 2"), callbackCaptor.capture()); + + callbackCaptor.getValue().onReceiveValue("da result"); + assertEquals(successValue[0], "da result"); + } + + @Test + public void getTitle() { + when(mockWebView.getTitle()).thenReturn("My title"); + assertEquals(testHostApiImpl.getTitle(0L), "My title"); + } + + @Test + public void scrollTo() { + testHostApiImpl.scrollTo(0L, 12L, 13L); + verify(mockWebView).scrollTo(12, 13); + } + + @Test + public void scrollBy() { + testHostApiImpl.scrollBy(0L, 15L, 23L); + verify(mockWebView).scrollBy(15, 23); + } + + @Test + public void getScrollX() { + when(mockWebView.getScrollX()).thenReturn(55); + assertEquals((long) testHostApiImpl.getScrollX(0L), 55); + } + + @Test + public void getScrollY() { + when(mockWebView.getScrollY()).thenReturn(23); + assertEquals((long) testHostApiImpl.getScrollY(0L), 23); + } + + @Test + public void setWebViewClient() { + final WebViewClient mockWebViewClient = mock(WebViewClient.class); + testInstanceManager.addInstance(mockWebViewClient, 1L); + + testHostApiImpl.setWebViewClient(0L, 1L); + verify(mockWebView).setWebViewClient(mockWebViewClient); + } + + @Test + public void addJavaScriptChannel() { + final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + testInstanceManager.addInstance(javaScriptChannel, 1L); + + testHostApiImpl.addJavaScriptChannel(0L, 1L); + verify(mockWebView).addJavascriptInterface(javaScriptChannel, "aName"); + } + + @Test + public void removeJavaScriptChannel() { + final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + testInstanceManager.addInstance(javaScriptChannel, 1L); + + testHostApiImpl.removeJavaScriptChannel(0L, 1L); + verify(mockWebView).removeJavascriptInterface("aName"); + } + + @Test + public void setDownloadListener() { + final DownloadListener mockDownloadListener = mock(DownloadListener.class); + testInstanceManager.addInstance(mockDownloadListener, 1L); + + testHostApiImpl.setDownloadListener(0L, 1L); + verify(mockWebView).setDownloadListener(mockDownloadListener); + } } diff --git a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh index d866473636cc..30a6918fc922 100755 --- a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh +++ b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh @@ -5,4 +5,6 @@ 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 +--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 From 984015dffebb7d39a12486dd36ffdb529a278332 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 27 Oct 2021 15:07:52 -0700 Subject: [PATCH 121/259] Partial revert of "upgraded usage of BinaryMessenger (#4451)" (#4453) This reverts commit 9d63517bdcf9b2be805d8ec9c4d218441c1de1e7. The work to accomodate the breaking change is no longer necessary. --- .cirrus.yml | 4 -- ci/stable_conditional.dart | 67 ------------------- .../MethodCallHandlerImplTest.java | 28 ++------ .../plugins/camera/DartMessengerTest.java | 21 +----- .../quickactions/QuickActionsTest.java | 22 +----- .../MethodCallHandlerImplTest.java | 28 ++------ 6 files changed, 12 insertions(+), 158 deletions(-) delete mode 100644 ci/stable_conditional.dart diff --git a/.cirrus.yml b/.cirrus.yml index 9776d9a2ab31..da354c5a9b22 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -197,10 +197,6 @@ task: - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh lint-android # must come after build-examples - stable_channel_conditional_script: - - if [[ "$CHANNEL" == "stable" ]]; then - - dart ./ci/stable_conditional.dart - - fi native_unit_test_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. diff --git a/ci/stable_conditional.dart b/ci/stable_conditional.dart deleted file mode 100644 index 76643532bc9d..000000000000 --- a/ci/stable_conditional.dart +++ /dev/null @@ -1,67 +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. -// -// stable_conditional.dart -// -// Performs simple find and replace operations for conditional compilation -// before executing stable channel tests. -// -// Example input: -// int main() { -// // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE -// printf("hello world\n"); -// // FLUTTER_STABLE_CONDITIONAL_ELSE -// // printf("goodbye world\n"); -// // FLUTTER_STABLE_CONDITIONAL_ENDIF -// } -// -// Example output: -// int main() { -// printf("goodbye world\n"); -// } - -import 'dart:convert' show LineSplitter; -import 'dart:io' show FileSystemEntity, File; - -final List _filesToProcess = [ - 'packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java', - 'packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java', - 'packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java', - 'packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java', -]; - -final RegExp _replacer = RegExp( - r'^\s*// FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE(.*?)^\s*// FLUTTER_STABLE_CONDITIONAL_ELSE(.*?)^\s*// FLUTTER_STABLE_CONDITIONAL_ENDIF', - multiLine: true, - dotAll: true); -final RegExp _commentRemover = RegExp(r'^(\s*)\/\/\s*(.*)'); -const String _newline = '\n'; - -void _process(FileSystemEntity entity) { - const LineSplitter splitter = LineSplitter(); - final String text = File(entity.path).readAsStringSync(); - String replaced = ''; - int index = 0; - for (final RegExpMatch match in _replacer.allMatches(text)) { - replaced += text.substring(index, match.start); - for (final String line in splitter.convert(match.group(2)!)) { - final RegExpMatch? commentRemoverMatch = _commentRemover.firstMatch(line); - if (commentRemoverMatch != null) { - replaced += commentRemoverMatch.group(1)! + - commentRemoverMatch.group(2)! + - _newline; - } - } - index = match.end; - } - if (replaced.isNotEmpty) { - replaced += text.substring(index, text.length); - File(entity.path).writeAsStringSync(replaced); - print('modified: ${entity.path}'); - } -} - -void main(List args) { - _filesToProcess.map((String path) => File(path)).forEach(_process); -} diff --git a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java index 012cc9be9711..0ea03a0690f1 100644 --- a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java @@ -55,11 +55,7 @@ public void startListening_registersChannel() { methodCallHandler.startListening(messenger); verify(messenger, times(1)) - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); } @Test @@ -71,15 +67,9 @@ public void startListening_unregistersExistingChannel() { methodCallHandler.startListening(secondMessenger); // Unregisters the first and then registers the second. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - // verify(secondMessenger, times(1)) - // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); } @Test @@ -89,11 +79,7 @@ public void stopListening_unregistersExistingChannel() { methodCallHandler.stopListening(); - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); } @Test @@ -102,11 +88,7 @@ public void stopListening_doesNothingWhenUnset() { methodCallHandler.stopListening(); - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null, null); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 1ce04d6cc104..0a2fc43d03cb 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -12,7 +12,6 @@ import android.os.Handler; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -32,15 +31,6 @@ public class DartMessengerTest { private static class FakeBinaryMessenger implements BinaryMessenger { private final List sentMessages = new ArrayList<>(); - // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - @Override - public BinaryMessenger.TaskQueue makeBackgroundTaskQueue() { - return null; - } - // FLUTTER_STABLE_CONDITIONAL_ELSE - // FLUTTER_STABLE_CONDITIONAL_ENDIF - @Override public void send(@NonNull String channel, ByteBuffer message) { sentMessages.add(message); @@ -51,17 +41,8 @@ public void send(@NonNull String channel, ByteBuffer message, BinaryReply callba send(channel, message); } - // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE @Override - public void setMessageHandler( - @NonNull String channel, - BinaryMessageHandler handler, - @Nullable BinaryMessenger.TaskQueue taskQueue) {} - // FLUTTER_STABLE_CONDITIONAL_ELSE - // @Override - // public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} - // FLUTTER_STABLE_CONDITIONAL_ENDIF + public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} List getMessages() { return new ArrayList<>(sentMessages); diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index 2b6fb495f7c4..208a119efafe 100644 --- a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -33,15 +33,6 @@ public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { public MethodCall lastMethodCall; - // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - @Override - public BinaryMessenger.TaskQueue makeBackgroundTaskQueue() { - return null; - } - // FLUTTER_STABLE_CONDITIONAL_ELSE - // FLUTTER_STABLE_CONDITIONAL_ENDIF - @Override public void send(@NonNull String channel, @Nullable ByteBuffer message) { send(channel, message, null); @@ -58,21 +49,10 @@ public void send( } } - // TODO(aaclarke): Remove when https://github.com/flutter/engine/pull/29147 is on stable. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE @Override - public void setMessageHandler( - @NonNull String channel, - @Nullable BinaryMessageHandler handler, - @Nullable BinaryMessenger.TaskQueue taskQueue) { + public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { // Do nothing. } - // FLUTTER_STABLE_CONDITIONAL_ELSE - // @Override - // public void setMessageHandler( - // @NonNull String channel, - // @Nullable BinaryMessageHandler handler) {} - // FLUTTER_STABLE_CONDITIONAL_ENDIF } static final int SUPPORTED_BUILD = 25; diff --git a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java index 6b544c7611c5..5e0811399ac6 100644 --- a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java +++ b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java @@ -44,11 +44,7 @@ public void startListening_registersChannel() { methodCallHandler.startListening(messenger); verify(messenger, times(1)) - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); } @Test @@ -60,15 +56,9 @@ public void startListening_unregistersExistingChannel() { methodCallHandler.startListening(secondMessenger); // Unregisters the first and then registers the second. - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class), eq(null)); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - // verify(secondMessenger, times(1)) - // .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); } @Test @@ -78,11 +68,7 @@ public void stopListening_unregistersExistingChannel() { methodCallHandler.stopListening(); - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null, null); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); } @Test @@ -91,11 +77,7 @@ public void stopListening_doesNothingWhenUnset() { methodCallHandler.stopListening(); - // FLUTTER_STABLE_CONDITIONAL_IF_NOT_STABLE - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null, null); - // FLUTTER_STABLE_CONDITIONAL_ELSE - // verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); - // FLUTTER_STABLE_CONDITIONAL_ENDIF + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); } @Test From bb62aaa4f384772152572bcbe710571dc3b1cd35 Mon Sep 17 00:00:00 2001 From: Nick Bradshaw Date: Wed, 27 Oct 2021 15:58:03 -0700 Subject: [PATCH 122/259] [webview_flutter] Implement zoom enabled for ios and android (#4417) --- .../webview_flutter_android/AUTHORS | 1 + .../webview_flutter_android/CHANGELOG.md | 4 +++ .../webviewflutter/FlutterWebView.java | 14 +++++++-- .../webviewflutter/WebViewBuilder.java | 17 ++++++++++ .../webviewflutter/FlutterWebViewTest.java | 2 ++ .../example/lib/web_view.dart | 13 ++++++++ .../webview_flutter_android/pubspec.yaml | 5 ++- .../webview_flutter_wkwebview/AUTHORS | 2 ++ .../webview_flutter_wkwebview/CHANGELOG.md | 4 +++ .../FLTWKNavigationDelegateTests.m | 31 +++++++++++++++++++ .../example/lib/web_view.dart | 9 +++++- .../ios/Classes/FLTWKNavigationDelegate.h | 7 ++++- .../ios/Classes/FLTWKNavigationDelegate.m | 11 +++++++ .../ios/Classes/FlutterWebView.m | 3 ++ .../webview_flutter_wkwebview/pubspec.yaml | 5 ++- 15 files changed, 117 insertions(+), 11 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS index 4461b602a13b..22e2b0ef78fc 100644 --- a/packages/webview_flutter/webview_flutter_android/AUTHORS +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -65,4 +65,5 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom +Nick Bradshaw diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index d4827a71e47d..c0837263509d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Add `zoomEnabled` functionality. + ## 2.0.15 * Added Overrides in FlutterWebView.java diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index ff573c771960..b2a453adcdac 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -176,9 +176,10 @@ static WebView createWebView( .setJavaScriptCanOpenWindowsAutomatically( true) // Always allow automatically opening of windows. .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient(webChromeClient) - .setDownloadListener( - downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. + .setWebChromeClient( + webChromeClient) // Always use {@link FlutterWebChromeClient} as web Chrome client. + .setDownloadListener(downloadListener) + .setZoomControlsEnabled(true); // Always use built-in zoom mechanisms. return webViewBuilder.build(); } @@ -428,6 +429,9 @@ private void applySettings(Map settings) { case "allowsInlineMediaPlayback": // no-op inline media playback is always allowed on Android. break; + case "zoomEnabled": + setZoomEnabled((boolean) settings.get(key)); + break; default: throw new IllegalArgumentException("Unknown WebView setting: " + key); } @@ -467,6 +471,10 @@ private void updateUserAgent(String userAgent) { webView.getSettings().setUserAgentString(userAgent); } + private void setZoomEnabled(boolean shouldEnable) { + webView.getSettings().setSupportZoom(shouldEnable); + } + @Override public void dispose() { methodChannel.setMethodCallHandler(null); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java index d3cd1d57cdae..e0d5e8815f31 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -46,6 +46,7 @@ static WebView create(Context context, boolean usesHybridComposition, View conta private boolean usesHybridComposition; private WebChromeClient webChromeClient; private DownloadListener downloadListener; + private boolean enableBuiltInZoomControls; /** * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link @@ -136,6 +137,18 @@ public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadLis return this; } + /** + * Sets whether the {@link WebView} should use its built-in zoom mechanisms. The default value is + * {@code true}. + * + * @param flag {@code true} if built in zoom controls are allowed. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setZoomControlsEnabled(boolean flag) { + this.enableBuiltInZoomControls = flag; + return this; + } + /** * Build the {@link android.webkit.WebView} using the current settings. * @@ -148,6 +161,10 @@ public WebView build() { webSettings.setDomStorageEnabled(enableDomStorage); webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); webSettings.setSupportMultipleWindows(supportMultipleWindows); + webSettings.setLoadWithOverviewMode(true); + webSettings.setUseWideViewPort(true); + webSettings.setDisplayZoomControls(false); + webSettings.setBuiltInZoomControls(enableBuiltInZoomControls); webView.setWebChromeClient(webChromeClient); webView.setDownloadListener(downloadListener); return webView; diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java index 56d9db1ee493..fd79bccabbce 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -37,6 +37,7 @@ public void before() { .thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setZoomControlsEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) .thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) @@ -55,6 +56,7 @@ public void createWebView_should_create_webview_with_default_configuration() { verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); + verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true); } private Map createParameterMap(boolean usesHybridComposition) { diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 33773f96cad8..5e8a2798b8ca 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -72,6 +72,7 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.zoomEnabled = true, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, @@ -221,6 +222,11 @@ class WebView extends StatefulWidget { /// By default `gestureNavigationEnabled` is false. final bool gestureNavigationEnabled; + /// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + /// The value used for the HTTP User-Agent: request header. /// /// When null the platform's webview default is used for the User-Agent header. @@ -553,12 +559,14 @@ class WebViewController { assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; WebSetting userAgent = WebSetting.absent(); + bool? zoomEnabled; if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -574,6 +582,9 @@ class WebViewController { if (currentValue.userAgent != newValue.userAgent) { userAgent = newValue.userAgent; } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } return WebSettings( javascriptMode: javascriptMode, @@ -581,6 +592,7 @@ class WebViewController { hasProgressTracking: hasProgressTracking, debuggingEnabled: debuggingEnabled, userAgent: userAgent, + zoomEnabled: zoomEnabled, ); } @@ -613,5 +625,6 @@ WebSettings _webSettingsFromWidget(WebView widget) { gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, ); } diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 04511e670d4c..577d37359af1 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.15 +version: 2.1.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,8 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - - webview_flutter_platform_interface: ^1.0.0 + webview_flutter_platform_interface: ^1.2.0 dev_dependencies: flutter_driver: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS index 78f9e5ad9f6b..4fa8b35fca8a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS +++ b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS @@ -65,3 +65,5 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom +Antonino Di Natale +Nick Bradshaw diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 242d79b4bd82..b75519af476c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Add `zoomEnabled` functionality. + ## 2.0.14 * Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m index eb6d1543ec07..a819a9b53d60 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -13,11 +13,19 @@ @interface FLTWKNavigationDelegateTests : XCTestCase @property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; @property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; +@property(strong, nonatomic) WKNavigation *navigation; @end @implementation FLTWKNavigationDelegateTests +NSString *const zoomDisablingJavascript = + @"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);"; + - (void)setUp { self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); self.navigationDelegate = @@ -38,4 +46,27 @@ - (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { } } +- (void)testWebViewWebEvaluateJavaScriptSourceIsCorrectWhenShouldEnableZoomIsFalse { + WKWebView *webview = OCMClassMock(WKWebView.class); + WKNavigation *navigation = OCMClassMock(WKNavigation.class); + NSURL *testUrl = [[NSURL alloc] initWithString:@"www.example.com"]; + OCMStub([webview URL]).andReturn(testUrl); + + self.navigationDelegate.shouldEnableZoom = false; + [self.navigationDelegate webView:webview didFinishNavigation:navigation]; + OCMVerify([webview evaluateJavaScript:zoomDisablingJavascript completionHandler:nil]); +} + +- (void)testWebViewWebEvaluateJavaScriptShouldNotBeCalledWhenShouldEnableZoomIsTrue { + WKWebView *webview = OCMClassMock(WKWebView.class); + WKNavigation *navigation = OCMClassMock(WKNavigation.class); + NSURL *testUrl = [[NSURL alloc] initWithString:@"www.example.com"]; + OCMStub([webview URL]).andReturn(testUrl); + + self.navigationDelegate.shouldEnableZoom = true; + + OCMReject([webview evaluateJavaScript:zoomDisablingJavascript completionHandler:nil]); + [self.navigationDelegate webView:webview didFinishNavigation:navigation]; +} + @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index ddb8e9b0f14f..d57499dcef50 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -4,9 +4,9 @@ import 'dart:async'; -import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; @@ -65,6 +65,7 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.zoomEnabled = true, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, @@ -213,6 +214,11 @@ class WebView extends StatefulWidget { /// By default `userAgent` is null. final String? userAgent; + /// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + /// Which restrictions apply on automatic media playback. /// /// This initial value is applied to the platform's webview upon creation. Any following @@ -542,6 +548,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, ); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h index 31edadc8cc05..6531931c4cf4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h @@ -9,13 +9,18 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTWKNavigationDelegate : NSObject -- (instancetype)initWithChannel:(FlutterMethodChannel*)channel; +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel; /** * Whether to delegate navigation decisions over the method channel. */ @property(nonatomic, assign) BOOL hasDartNavigationDelegate; +/** + * Whether to allow zoom functionality on the WebView. + */ +@property(nonatomic, assign) BOOL shouldEnableZoom; + @end NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m index 8b7ee7d0cfb7..125d3cabdcf1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m @@ -63,6 +63,17 @@ - (void)webView:(WKWebView *)webView } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + if (!self.shouldEnableZoom) { + NSString *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);"; + + [webView evaluateJavaScript:source completionHandler:nil]; + } + [_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}]; } 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 c6d926d3cfc2..7fd78ee302d0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -348,6 +348,9 @@ - (NSString*)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"userAgent"]) { NSString* userAgent = settings[key]; [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; + } else if ([key isEqualToString:@"zoomEnabled"]) { + NSNumber* zoomEnabled = settings[key]; + _navigationDelegate.shouldEnableZoom = [zoomEnabled boolValue]; } else { [unknownKeys addObject:key]; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index a7305cea7a94..bfa4b8050e90 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.14 +version: 2.1.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,8 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - - webview_flutter_platform_interface: ^1.0.0 + webview_flutter_platform_interface: ^1.2.0 dev_dependencies: flutter_driver: From 76e84c0679dbab7bfaaaa553b17bb0dbdb9a3c33 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 27 Oct 2021 16:03:03 -0700 Subject: [PATCH 123/259] [video_player] Initialize player when size and duration become available (#4438) --- .../video_player/video_player/CHANGELOG.md | 4 ++ .../ios/Runner.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../ios/RunnerUITests/VideoPlayerUITests.m | 39 ++++++++++++------- .../video_player/example/lib/main.dart | 1 + .../ios/Classes/FLTVideoPlayerPlugin.m | 25 ++++++++++-- .../video_player/video_player/pubspec.yaml | 2 +- 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 539a5520e5be..9eb9d24679d0 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.6 + +* Initialize player when size and duration become available on iOS + ## 2.2.5 * Support to closed caption WebVTT format added. diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 2921ef9161be..d19d578b6fb4 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3f1ee9541e2f..0632b6533bc8 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.14.0 <3.0.0" From 34caa5f6d03772275919d119729041368f71909c Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Wed, 27 Oct 2021 16:08:02 -0700 Subject: [PATCH 124/259] [ci.yaml] Main branch support (#4440) --- .ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 00aecdca1122..2cd36d5b36dd 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -4,8 +4,9 @@ # for every commit. # # More information at: -# * https://github.com/flutter/cocoon/blob/master/CI_YAML.md +# * https://github.com/flutter/cocoon/blob/main/CI_YAML.md enabled_branches: + - main - master platform_properties: From 9ac6da7bb67c63e1f7ac92ea7fbe76f3adb8f106 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 27 Oct 2021 17:05:49 -0700 Subject: [PATCH 125/259] [device_info] started using Background Platform Channels (#4456) --- packages/device_info/device_info/CHANGELOG.md | 3 ++- .../plugins/deviceinfo/DeviceInfoPlugin.java | 26 +++++++++++++++++-- packages/device_info/device_info/pubspec.yaml | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 97349d450cf1..cdcb906da2fa 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.0.3 * Remove references to the Android V1 embedding. * Updated Android lint settings. +* Started using Background Platform Channels when available. ## 2.0.2 diff --git a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java index 9b766d7f8381..756f5e0ce820 100644 --- a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java +++ b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java @@ -5,13 +5,18 @@ package io.flutter.plugins.deviceinfo; import android.content.Context; +import android.util.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodCodec; +import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; /** DeviceInfoPlugin */ public class DeviceInfoPlugin implements FlutterPlugin { - + static final String TAG = "DeviceInfoPlugin"; MethodChannel channel; /** Plugin registration. */ @@ -32,7 +37,24 @@ public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { } private void setupMethodChannel(BinaryMessenger messenger, Context context) { - channel = new MethodChannel(messenger, "plugins.flutter.io/device_info"); + String channelName = "plugins.flutter.io/device_info"; + // TODO(gaaclarke): Remove reflection guard when https://github.com/flutter/engine/pull/29147 + // becomes available on the stable branch. + 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); + Log.d(TAG, "Use TaskQueues."); + } catch (Exception ex) { + channel = new MethodChannel(messenger, channelName); + Log.d(TAG, "Don't use TaskQueues."); + } final MethodCallHandlerImpl handler = new MethodCallHandlerImpl(context.getContentResolver(), context.getPackageManager()); channel.setMethodCallHandler(handler); diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml index c5830f401039..d557e496dcab 100644 --- a/packages/device_info/device_info/pubspec.yaml +++ b/packages/device_info/device_info/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on. repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" From 93f04832c6224f47325c4f028a5861882491580b Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 27 Oct 2021 17:06:06 -0700 Subject: [PATCH 126/259] [path_provider] started supporting background platform channels (#4443) * [path_provider] started supporting background platform channels * added docstrings --- .../path_provider/path_provider/CHANGELOG.md | 5 + .../pathprovider/PathProviderPlugin.java | 196 ++++++++++++++---- .../path_provider/path_provider/pubspec.yaml | 2 +- 3 files changed, 158 insertions(+), 45 deletions(-) diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 764662d5d84b..568dd2fde499 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.6 + +* Added support for Background Platform Channels on Android when it is + available. + ## 2.0.5 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index 49360809e892..983f6ff8719f 100644 --- a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -9,18 +9,24 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Looper; +import android.util.Log; import androidx.annotation.NonNull; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.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; @@ -28,32 +34,158 @@ import java.util.concurrent.Executors; public class PathProviderPlugin implements FlutterPlugin, MethodCallHandler { - + static final String TAG = "PathProviderPlugin"; private Context context; private MethodChannel channel; - private final Executor uiThreadExecutor = new UiThreadExecutor(); - private final Executor executor = - Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder() - .setNameFormat("path-provider-background-%d") - .setPriority(Thread.NORM_PRIORITY) - .build()); + private PathProviderImpl impl; + + /** + * An abstraction over how to access the paths in a thread-safe manner. + * + *

We need this so on versions of Flutter that support Background Platform Channels this plugin + * can take advantage of it. + * + *

This can be removed after https://github.com/flutter/engine/pull/29147 becomes available on + * the stable branch. + */ + private interface PathProviderImpl { + void getTemporaryDirectory(@NonNull Result result); + + void getApplicationDocumentsDirectory(@NonNull Result result); + + void getStorageDirectory(@NonNull Result result); + + void getExternalCacheDirectories(@NonNull Result result); + + void getExternalStorageDirectories(@NonNull String directoryName, @NonNull Result result); + + void getApplicationSupportDirectory(@NonNull Result result); + } + + /** The implementation for getting system paths that executes from the platform */ + private class PathProviderPlatformThread implements PathProviderImpl { + private final Executor uiThreadExecutor = new UiThreadExecutor(); + private final Executor executor = + Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setNameFormat("path-provider-background-%d") + .setPriority(Thread.NORM_PRIORITY) + .build()); + + public void getTemporaryDirectory(@NonNull Result result) { + executeInBackground(() -> getPathProviderTemporaryDirectory(), result); + } + + public void getApplicationDocumentsDirectory(@NonNull Result result) { + executeInBackground(() -> getPathProviderApplicationDocumentsDirectory(), result); + } + + public void getStorageDirectory(@NonNull Result result) { + executeInBackground(() -> getPathProviderStorageDirectory(), result); + } + + public void getExternalCacheDirectories(@NonNull Result result) { + executeInBackground(() -> getPathProviderExternalCacheDirectories(), result); + } + + public void getExternalStorageDirectories( + @NonNull String directoryName, @NonNull Result result) { + executeInBackground(() -> getPathProviderExternalStorageDirectories(directoryName), result); + } + + public void getApplicationSupportDirectory(@NonNull Result result) { + executeInBackground(() -> PathProviderPlugin.this.getApplicationSupportDirectory(), result); + } + + private void executeInBackground(Callable task, Result result) { + final SettableFuture future = SettableFuture.create(); + Futures.addCallback( + future, + new FutureCallback() { + public void onSuccess(T answer) { + result.success(answer); + } + + public void onFailure(Throwable t) { + result.error(t.getClass().getName(), t.getMessage(), null); + } + }, + uiThreadExecutor); + executor.execute( + () -> { + try { + future.set(task.call()); + } catch (Throwable t) { + future.setException(t); + } + }); + } + } + + /** The implementation for getting system paths that executes from a background thread. */ + private class PathProviderBackgroundThread implements PathProviderImpl { + public void getTemporaryDirectory(@NonNull Result result) { + result.success(getPathProviderTemporaryDirectory()); + } + + public void getApplicationDocumentsDirectory(@NonNull Result result) { + result.success(getPathProviderApplicationDocumentsDirectory()); + } + + public void getStorageDirectory(@NonNull Result result) { + result.success(getPathProviderStorageDirectory()); + } + + public void getExternalCacheDirectories(@NonNull Result result) { + result.success(getPathProviderExternalCacheDirectories()); + } + + public void getExternalStorageDirectories( + @NonNull String directoryName, @NonNull Result result) { + result.success(getPathProviderExternalStorageDirectories(directoryName)); + } + + public void getApplicationSupportDirectory(@NonNull Result result) { + result.success(PathProviderPlugin.this.getApplicationSupportDirectory()); + } + } public PathProviderPlugin() {} + private void setup(BinaryMessenger messenger, Context context) { + String channelName = "plugins.flutter.io/path_provider"; + // TODO(gaaclarke): Remove reflection guard when https://github.com/flutter/engine/pull/29147 + // becomes available on the stable branch. + 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); + 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."); + } + this.context = context; + channel.setMethodCallHandler(this); + } + @SuppressWarnings("deprecation") public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { PathProviderPlugin instance = new PathProviderPlugin(); - instance.channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/path_provider"); - instance.context = registrar.context(); - instance.channel.setMethodCallHandler(instance); + instance.setup(registrar.messenger(), registrar.context()); } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - channel = new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/path_provider"); - context = binding.getApplicationContext(); - channel.setMethodCallHandler(this); + setup(binding.getBinaryMessenger(), binding.getApplicationContext()); } @Override @@ -62,52 +194,28 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel = null; } - private void executeInBackground(Callable task, Result result) { - final SettableFuture future = SettableFuture.create(); - Futures.addCallback( - future, - new FutureCallback() { - public void onSuccess(T answer) { - result.success(answer); - } - - public void onFailure(Throwable t) { - result.error(t.getClass().getName(), t.getMessage(), null); - } - }, - uiThreadExecutor); - executor.execute( - () -> { - try { - future.set(task.call()); - } catch (Throwable t) { - future.setException(t); - } - }); - } - @Override public void onMethodCall(MethodCall call, @NonNull Result result) { switch (call.method) { case "getTemporaryDirectory": - executeInBackground(() -> getPathProviderTemporaryDirectory(), result); + impl.getTemporaryDirectory(result); break; case "getApplicationDocumentsDirectory": - executeInBackground(() -> getPathProviderApplicationDocumentsDirectory(), result); + impl.getApplicationDocumentsDirectory(result); break; case "getStorageDirectory": - executeInBackground(() -> getPathProviderStorageDirectory(), result); + impl.getStorageDirectory(result); break; case "getExternalCacheDirectories": - executeInBackground(() -> getPathProviderExternalCacheDirectories(), result); + impl.getExternalCacheDirectories(result); break; case "getExternalStorageDirectories": final Integer type = call.argument("type"); final String directoryName = StorageDirectoryMapper.androidType(type); - executeInBackground(() -> getPathProviderExternalStorageDirectories(directoryName), result); + impl.getExternalStorageDirectories(directoryName, result); break; case "getApplicationSupportDirectory": - executeInBackground(() -> getApplicationSupportDirectory(), result); + impl.getApplicationSupportDirectory(result); break; default: result.notImplemented(); diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 5e9bc0b0e7c4..12a186902736 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.5 +version: 2.0.6 environment: sdk: ">=2.14.0 <3.0.0" From 34ea0c3c1a0963cdd6d689f9b1ca861b3d3f82b8 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 28 Oct 2021 10:54:50 -0700 Subject: [PATCH 127/259] Support Hybrid Composition through the GoogleMaps Widget (#4082) --- .../google_maps_flutter/CHANGELOG.md | 5 ++ .../google_maps_flutter/README.md | 12 +++++ .../google_maps_flutter/example/lib/main.dart | 6 +++ .../lib/src/google_map.dart | 50 ++++++++++++++++++- .../google_maps_flutter/pubspec.yaml | 2 +- .../test/google_map_test.dart | 31 +++++++----- 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 0c95abda319b..4bc4ce32a169 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.0 + +* Add iOS unit and UI integration test targets. +* Provide access to Hybrid Composition on Android through the `GoogleMap` widget. + ## 2.0.11 * Add additional marker drag events. diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 99c04f3ae1df..038126f4fdd0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -46,6 +46,18 @@ This means that app will only be available for users that run Android SDK 20 or android:value="YOUR KEY HERE"/> ``` +#### Hybrid Composition + +To use [Hybrid Composition](https://flutter.dev/docs/development/platform-integration/platform-views) +to render the `GoogleMap` widget on Android, set `AndroidGoogleMapsFlutter.useAndroidViewSurface` to +true. + +```dart +if (defaultTargetPlatform == TargetPlatform.android) { + AndroidGoogleMapsFlutter.useAndroidViewSurface = true; +} +``` + ### iOS This plugin requires iOS 9.0 or higher. To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 15b14db0357a..f67e4ff8e563 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -4,7 +4,10 @@ // ignore_for_file: public_member_api_docs +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; import 'map_click.dart'; @@ -66,5 +69,8 @@ class MapsDemo extends StatelessWidget { } void main() { + if (defaultTargetPlatform == TargetPlatform.android) { + AndroidGoogleMapsFlutter.useAndroidViewSurface = true; + } runApp(MaterialApp(home: MapsDemo())); } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index b4ffd22360e8..b153832426dd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -38,6 +38,44 @@ class UnknownMapObjectIdError extends Error { } } +/// Android specific settings for [GoogleMap]. +class AndroidGoogleMapsFlutter { + /// Whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to false. + static bool get useAndroidViewSurface { + final GoogleMapsFlutterPlatform platform = + GoogleMapsFlutterPlatform.instance; + if (platform is MethodChannelGoogleMapsFlutter) { + return platform.useAndroidViewSurface; + } + return false; + } + + /// Set whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to false. + static set useAndroidViewSurface(bool useAndroidViewSurface) { + final GoogleMapsFlutterPlatform platform = + GoogleMapsFlutterPlatform.instance; + if (platform is MethodChannelGoogleMapsFlutter) { + platform.useAndroidViewSurface = useAndroidViewSurface; + } + } +} + /// A widget which displays a map with data obtained from the Google Maps service. class GoogleMap extends StatefulWidget { /// Creates a widget displaying data from Google Maps services. @@ -61,6 +99,7 @@ class GoogleMap extends StatefulWidget { this.tiltGesturesEnabled = true, this.myLocationEnabled = false, this.myLocationButtonEnabled = true, + this.layoutDirection, /// If no padding is specified default padding will be 0. this.padding = const EdgeInsets.all(0), @@ -100,6 +139,12 @@ class GoogleMap extends StatefulWidget { /// Type of map tiles to be rendered. final MapType mapType; + /// The layout direction to use for the embedded view. + /// + /// If this is null, the ambient [Directionality] is used instead. If there is + /// no ambient [Directionality], [TextDirection.ltr] is used. + final TextDirection? layoutDirection; + /// Preferred bounds for the camera zoom level. /// /// Actual bounds depend on map data and device. @@ -250,9 +295,12 @@ class _GoogleMapState extends State { @override Widget build(BuildContext context) { - return GoogleMapsFlutterPlatform.instance.buildView( + return GoogleMapsFlutterPlatform.instance.buildViewWithTextDirection( _mapId, onPlatformViewCreated, + textDirection: widget.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, initialCameraPosition: widget.initialCameraPosition, markers: widget.markers, polygons: widget.polygons, diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 61ac88a8f10d..dae5a28319ab 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/master/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.0.11 +version: 2.1.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index d1ec87a4730d..003ae06d9877 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'fake_maps_controllers.dart'; @@ -604,17 +605,21 @@ void main() { }, ); - // TODO(bparrishMines): Uncomment once https://github.com/flutter/plugins/pull/4017 has landed. - // testWidgets('Use AndroidViewSurface on Android', (WidgetTester tester) async { - // await tester.pumpWidget( - // const Directionality( - // textDirection: TextDirection.ltr, - // child: GoogleMap( - // initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - // ), - // ), - // ); - // - // expect(find.byType(AndroidViewSurface), findsOneWidget); - // }); + testWidgets('Use PlatformViewLink on Android', (WidgetTester tester) async { + final MethodChannelGoogleMapsFlutter platform = + GoogleMapsFlutterPlatform.instance as MethodChannelGoogleMapsFlutter; + platform.useAndroidViewSurface = true; + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + expect(find.byType(PlatformViewLink), findsOneWidget); + platform.useAndroidViewSurface = false; + }); } From 03622197cc1efe56e8c243e3e491d9120c91a6b0 Mon Sep 17 00:00:00 2001 From: Konoshenko Vlad Date: Fri, 29 Oct 2021 00:08:03 +0300 Subject: [PATCH 128/259] [image_picker] doc:readme image picker misprints (#4421) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/README.md | 2 +- packages/image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 90829cc7ae6a..69a7660abc76 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4+4 + +* Fix typos in README.md. + ## 0.8.4+3 * Suppress a unchecked cast build warning. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index d8f5835fd402..100606c3658a 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -47,7 +47,7 @@ import 'package:image_picker/image_picker.dart'; // Pick a video final XFile? image = await _picker.pickVideo(source: ImageSource.gallery); // Capture a video - final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); + final XFile? video = await _picker.pickVideo(source: ImageSource.camera); // Pick multiple images final List? images = await _picker.pickMultiImage(); ... diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 99766702cfdf..44a7c9b80749 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/master/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.4+3 +version: 0.8.4+4 environment: sdk: ">=2.14.0 <3.0.0" From e51cc1df7b75ddac344eb8f8835863c7c2f53055 Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:33:05 +0200 Subject: [PATCH 129/259] [in_app_purchase]IAP/platform interface add cancel status (#4093) --- .../CHANGELOG.md | 6 +++++- .../lib/src/types/purchase_status.dart | 5 +++++ .../pubspec.yaml | 2 +- .../test/src/types/product_details_test.dart | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) 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 cd4b86d7f39a..e01f5277b6f1 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 @@ +## 1.3.0 + +* Added new `PurchaseStatus` named `canceled` to distinguish between an error and user cancellation. + ## 1.2.0 * Added `toString()` to `IAPError` @@ -12,4 +16,4 @@ ## 1.0.0 -* Initial open-source release. \ No newline at end of file +* Initial open-source release. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart index 78695066702d..ed54e97442a2 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -26,4 +26,9 @@ enum PurchaseStatus { /// the purchase by calling the `completePurchase` method. More information on /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#restoring-previous-purchases). restored, + + /// The purchase has been canceled. + /// + /// Update your UI to indicate the purchase is canceled. + canceled, } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index 64574e0cf306..fc306acbd41e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purch issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.2.0 +version: 1.3.0 environment: sdk: ">=2.12.0 <3.0.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 ce49d9992131..737f0d00b392 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,6 +4,7 @@ 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', () { @@ -27,4 +28,22 @@ void main() { expect(productDetails.currencySymbol, r'$'); }); }); + + group('PurchaseStatus Tests', () { + test('PurchaseStatus should contain 5 options', () { + const List values = PurchaseStatus.values; + + expect(values.length, 5); + }); + + test('PurchaseStatus enum should have items in correct index', () { + const List values = PurchaseStatus.values; + + expect(values[0], PurchaseStatus.pending); + expect(values[1], PurchaseStatus.purchased); + expect(values[2], PurchaseStatus.error); + expect(values[3], PurchaseStatus.restored); + expect(values[4], PurchaseStatus.canceled); + }); + }); } From 7588dd9cfe0b8e6ca5292389723682ab26258149 Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Sat, 30 Oct 2021 02:23:03 +0800 Subject: [PATCH 130/259] [google_sign_in] Use a transparent image as a placeholder for the `GoogleUserCircleAvatar` (#4391) --- .../google_sign_in/CHANGELOG.md | 4 + .../google_sign_in/lib/widgets.dart | 14 ++- .../google_sign_in/pubspec.yaml | 2 +- .../google_sign_in/resources/README.md | 7 ++ .../resources/transparentImage.gif | Bin 0 -> 42 bytes .../google_sign_in/test/widgets_test.dart | 117 ++++++++++++++++++ 6 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/resources/README.md create mode 100644 packages/google_sign_in/google_sign_in/resources/transparentImage.gif create mode 100644 packages/google_sign_in/google_sign_in/test/widgets_test.dart diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 6f4ccebf8dc3..b16b5f9f5e79 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.2.1 + + Change the placeholder of the GoogleUserCircleAvatar to a transparent image. + ## 5.2.0 * Add `GoogleSignInAccount.serverAuthCode`. Mark `GoogleSignInAuthentication.serverAuthCode` as deprecated. diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 18f9973454f6..c031cb2b6eca 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -106,10 +106,22 @@ class GoogleUserCircleAvatar extends StatelessWidget { FadeInImage.memoryNetwork( // This creates a transparent placeholder image, so that // [placeholder] shows through. - placeholder: Uint8List((size.round() * size.round())), + placeholder: _transparentImage, image: sizedPhotoUrl, ) ]), )); } } + +/// This is an transparent 1x1 gif image. +/// +/// Those bytes come from `resources/transparentImage.gif`. +final Uint8List _transparentImage = Uint8List.fromList( + [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x21, 0xf9, 0x04, 0x01, 0x00, // + 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, // + 0x00, 0x02, 0x01, 0x44, 0x00, 0x3B + ], +); diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 91114c6b0491..03e583a662d8 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/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 and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.2.0 +version: 5.2.1 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/google_sign_in/google_sign_in/resources/README.md b/packages/google_sign_in/google_sign_in/resources/README.md new file mode 100644 index 000000000000..b3f0383a6695 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/resources/README.md @@ -0,0 +1,7 @@ +`transparentImage.gif` is a 1x1 transparent gif which comes from [this wikimedia page](https://commons.wikimedia.org/wiki/File:Transparent.gif): + +![](transparentImage.gif) + +This is the image used a placeholder for the `GoogleCircleAvatar` widget. + +The variable `_transparentImage` in `lib/widgets.dart` is the list of bytes of `transparentImage.gif`. \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in/resources/transparentImage.gif b/packages/google_sign_in/google_sign_in/resources/transparentImage.gif new file mode 100644 index 0000000000000000000000000000000000000000..f191b280ce91e6cb8c387735c10ef9bc5da6c83b GIT binary patch literal 42 ocmZ?wbhEHbWMp7uXkY+=|Ns9h{$ybUF?B!$NQQxl(S^Yp0J!f4_W%F@ literal 0 HcmV?d00001 diff --git a/packages/google_sign_in/google_sign_in/test/widgets_test.dart b/packages/google_sign_in/google_sign_in/test/widgets_test.dart new file mode 100644 index 000000000000..f7bd6f803e7c --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/widgets_test.dart @@ -0,0 +1,117 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +/// A instantiable class that extends [GoogleIdentity] +class _TestGoogleIdentity extends GoogleIdentity { + _TestGoogleIdentity({ + required this.id, + required this.email, + this.photoUrl, + }); + + final String id; + final String email; + + final String? photoUrl; + + @override + String? get displayName => null; + + @override + String? get serverAuthCode => null; +} + +/// A mocked [HttpClient] which always returns a [_MockHttpRequest]. +class _MockHttpClient extends Fake implements HttpClient { + @override + bool autoUncompress = true; + + @override + Future getUrl(Uri url) { + return Future.value(_MockHttpRequest()); + } +} + +/// A mocked [HttpClientRequest] which always returns a [_MockHttpClientResponse]. +class _MockHttpRequest extends Fake implements HttpClientRequest { + @override + Future close() { + return Future.value(_MockHttpResponse()); + } +} + +/// Arbitrary valid image returned by the [_MockHttpResponse]. +/// +/// This is an transparent 1x1 gif image. +/// It doesn't have to match the placeholder used in [GoogleUserCircleAvatar]. +/// +/// Those bytes come from `resources/transparentImage.gif`. +final Uint8List _transparentImage = Uint8List.fromList( + [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x21, 0xf9, 0x04, 0x01, 0x00, // + 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, // + 0x00, 0x02, 0x01, 0x44, 0x00, 0x3B + ], +); + +/// A mocked [HttpClientResponse] which is empty and has a [statusCode] of 200 +/// and returns valid image. +class _MockHttpResponse extends Fake implements HttpClientResponse { + final Stream _delegate = + Stream.value(_transparentImage); + + @override + int get contentLength => -1; + + @override + HttpClientResponseCompressionState get compressionState { + return HttpClientResponseCompressionState.decompressed; + } + + @override + StreamSubscription listen(void Function(Uint8List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return _delegate.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + int get statusCode => 200; +} + +void main() { + testWidgets('It should build the GoogleUserCircleAvatar successfully', + (WidgetTester tester) async { + final GoogleIdentity identity = _TestGoogleIdentity( + email: 'email@email.com', + id: 'userId', + photoUrl: 'photoUrl', + ); + tester.binding.window.physicalSizeTestValue = Size(100, 100); + + await HttpOverrides.runZoned( + () async { + await tester.pumpWidget(MaterialApp( + home: SizedBox( + height: 100, + width: 100, + child: GoogleUserCircleAvatar( + identity: identity, + ), + ), + )); + }, + createHttpClient: (SecurityContext? c) => _MockHttpClient(), + ); + }); +} From 29f46b45a9ec7f475ee064880830d38e4f6bedc3 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Fri, 29 Oct 2021 16:23:02 -0700 Subject: [PATCH 131/259] [camera] Fix CamcorderProfile Usages (#4423) --- packages/camera/camera/CHANGELOG.md | 3 + packages/camera/camera/android/build.gradle | 9 +- .../io/flutter/plugins/camera/Camera.java | 19 +- .../resolution/ResolutionFeature.java | 104 ++++++++- .../camera/media/MediaRecorderBuilder.java | 70 ++++-- .../io/flutter/plugins/camera/CameraTest.java | 15 -- .../CameraTest_getRecordingProfileTest.java | 205 ++++++++++++++++++ .../resolution/ResolutionFeatureTest.java | 168 ++++++++++++-- .../DeviceOrientationManagerTest.java | 1 + .../media/MediaRecorderBuilderTest.java | 127 ++++++++++- .../src/test/resources/robolectric.properties | 1 + .../camera/example/android/app/build.gradle | 2 +- .../camera/example/android/gradle.properties | 2 +- 13 files changed, 659 insertions(+), 67 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java create mode 100644 packages/camera/camera/android/src/test/resources/robolectric.properties diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index e6495a8fc796..29750f9cefff 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.9.4+3 +* Change Android compileSdkVersion to 31. +* Remove usages of deprecated Android API `CamcorderProfile`. +* Update gradle version to 7.0.2 on Android. * Fix registerTexture and result being called on background thread on iOS. ## 0.9.4+2 diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 25285ad33205..264f7f8edc7d 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.2' } } @@ -27,9 +27,10 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { + targetSdkVersion 31 minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -60,7 +61,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.12.4' + testImplementation 'org.mockito:mockito-inline:4.0.0' testImplementation 'androidx.test:core:1.3.0' - testImplementation 'org.robolectric:robolectric:4.3' + testImplementation 'org.robolectric:robolectric:4.5' } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 75ced531b08a..e31903955b4e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -20,6 +20,7 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; @@ -199,8 +200,16 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) .getLockedCaptureOrientation(); + MediaRecorderBuilder mediaRecorderBuilder; + + if (Build.VERSION.SDK_INT >= 31) { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath); + } else { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath); + } + mediaRecorder = - new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + mediaRecorderBuilder .setEnableAudio(enableAudio) .setMediaOrientation( lockedOrientation == null @@ -918,8 +927,12 @@ public float getMinZoomLevel() { return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - /** Shortcut to get current recording profile. */ - CamcorderProfile getRecordingProfile() { + /** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */ + CamcorderProfile getRecordingProfileLegacy() { + return cameraFeatures.getResolution().getRecordingProfileLegacy(); + } + + EncoderProfiles getRecordingProfile() { return cameraFeatures.getResolution().getRecordingProfile(); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index 67763dde9be4..afbd7c3758a6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -4,12 +4,16 @@ package io.flutter.plugins.camera.features.resolution; +import android.annotation.TargetApi; import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Build; import android.util.Size; import androidx.annotation.VisibleForTesting; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; +import java.util.List; /** * Controls the resolutions configuration on the {@link android.hardware.camera2} API. @@ -21,7 +25,8 @@ public class ResolutionFeature extends CameraFeature { private Size captureSize; private Size previewSize; - private CamcorderProfile recordingProfile; + private CamcorderProfile recordingProfileLegacy; + private EncoderProfiles recordingProfile; private ResolutionPreset currentSetting; private int cameraId; @@ -51,7 +56,11 @@ public ResolutionFeature( * * @return Resolution information to configure the {@link android.hardware.camera2} API. */ - public CamcorderProfile getRecordingProfile() { + public CamcorderProfile getRecordingProfileLegacy() { + return this.recordingProfileLegacy; + } + + public EncoderProfiles getRecordingProfile() { return this.recordingProfile; } @@ -100,19 +109,29 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { } @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) + throws IndexOutOfBoundsException { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + List videoProfiles = profile.getVideoProfiles(); + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } } /** * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link - * ResolutionPreset}. + * ResolutionPreset}. Supports SDK < 31. * * @param cameraId Camera identifier which indicates the device's camera for which to select a * {@link android.media.CamcorderProfile}. @@ -121,7 +140,7 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied * {@link ResolutionPreset}. */ - public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy( int cameraId, ResolutionPreset preset) { if (cameraId < 0) { throw new AssertionError( @@ -164,13 +183,74 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres } } - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + @TargetApi(Build.VERSION_CODES.S) + public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + String cameraIdString = Integer.toString(cameraId); + + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW); + } + + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) + throws IndexOutOfBoundsException { if (!checkIsSupported()) { return; } - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + + if (Build.VERSION.SDK_INT >= 31) { + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + List videoProfiles = recordingProfile.getVideoProfiles(); + + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile camcorderProfile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); + recordingProfileLegacy = camcorderProfile; + captureSize = + new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight); + } + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index a78c2b47b7ad..0aebfee39e0a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -5,11 +5,14 @@ package io.flutter.plugins.camera.media; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.MediaRecorder; +import android.os.Build; import androidx.annotation.NonNull; import java.io.IOException; public class MediaRecorderBuilder { + @SuppressWarnings("deprecation") static class MediaRecorderFactory { MediaRecorder makeMediaRecorder() { return new MediaRecorder(); @@ -17,23 +20,40 @@ MediaRecorder makeMediaRecorder() { } private final String outputFilePath; - private final CamcorderProfile recordingProfile; + private final CamcorderProfile camcorderProfile; + private final EncoderProfiles encoderProfiles; private final MediaRecorderFactory recorderFactory; private boolean enableAudio; private int mediaOrientation; public MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) { - this(recordingProfile, outputFilePath, new MediaRecorderFactory()); + @NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath) { + this(camcorderProfile, outputFilePath, new MediaRecorderFactory()); + } + + public MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, @NonNull String outputFilePath) { + this(encoderProfiles, outputFilePath, new MediaRecorderFactory()); } MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, + @NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath, MediaRecorderFactory helper) { this.outputFilePath = outputFilePath; - this.recordingProfile = recordingProfile; + this.camcorderProfile = camcorderProfile; + this.encoderProfiles = null; + this.recorderFactory = helper; + } + + MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, + @NonNull String outputFilePath, + MediaRecorderFactory helper) { + this.outputFilePath = outputFilePath; + this.encoderProfiles = encoderProfiles; + this.camcorderProfile = null; this.recorderFactory = helper; } @@ -47,23 +67,43 @@ public MediaRecorderBuilder setMediaOrientation(int orientation) { return this; } - public MediaRecorder build() throws IOException { + public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException { MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); // There's a fixed order that mediaRecorder expects. Only change these functions accordingly. // You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder. if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) { - mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate); - mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); + + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0); + EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0); + + mediaRecorder.setOutputFormat(encoderProfiles.getRecommendedFileFormat()); + if (enableAudio) { + mediaRecorder.setAudioEncoder(audioProfile.getCodec()); + mediaRecorder.setAudioEncodingBitRate(audioProfile.getBitrate()); + mediaRecorder.setAudioSamplingRate(audioProfile.getSampleRate()); + } + mediaRecorder.setVideoEncoder(videoProfile.getCodec()); + mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate()); + mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + } else { + mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); + if (enableAudio) { + mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec); + mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate); + mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate); + } + mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec); + mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate); + mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate); + mediaRecorder.setVideoSize( + camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight); } - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + mediaRecorder.setOutputFile(outputFilePath); mediaRecorder.setOrientationHint(this.mediaOrientation); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9d973195435e..1ed2e4c11d7b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -22,7 +22,6 @@ import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; -import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; @@ -249,20 +248,6 @@ public void getMinZoomLevel() { assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); } - @Test - public void getRecordingProfile() { - ResolutionFeature mockResolutionFeature = - mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); - CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); - - when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); - - CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); - - verify(mockResolutionFeature, times(1)).getRecordingProfile(); - assertEquals(mockCamcorderProfile, actualRecordingProfile); - } - @Test public void setExposureMode_shouldUpdateExposureLockFeature() { ExposureLockFeature mockExposureLockFeature = diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java new file mode 100644 index 000000000000..04bab14f26ac --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java @@ -0,0 +1,205 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class CameraTest_getRecordingProfileTest { + + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + } + + @Config(maxSdk = 30) + @Test + public void getRecordingProfileLegacy() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfileLegacy()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfileLegacy(); + + verify(mockResolutionFeature, times(1)).getRecordingProfileLegacy(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Config(minSdk = 31) + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + EncoderProfiles mockRecordingProfile = mock(EncoderProfiles.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockRecordingProfile); + + EncoderProfiles actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockRecordingProfile, actualRecordingProfile); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index e09223dfabe9..957b57a66435 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -8,24 +8,33 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import io.flutter.plugins.camera.CameraProperties; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) public class ResolutionFeatureTest { private static final String cameraName = "1"; - private CamcorderProfile mockProfileLow; + private CamcorderProfile mockProfileLowLegacy; + private EncoderProfiles mockProfileLow; private MockedStatic mockedStaticProfile; @Before - public void before() { + @SuppressWarnings("deprecation") + public void beforeLegacy() { mockedStaticProfile = mockStatic(CamcorderProfile.class); - mockProfileLow = mock(CamcorderProfile.class); - CamcorderProfile mockProfile = mock(CamcorderProfile.class); + mockProfileLowLegacy = mock(CamcorderProfile.class); + CamcorderProfile mockProfileLegacy = mock(CamcorderProfile.class); mockedStaticProfile .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) @@ -51,25 +60,58 @@ public void before() { mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) - .thenReturn(mockProfile); + .thenReturn(mockProfileLegacy); mockedStaticProfile .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLowLegacy); + } + + public void before() { + mockProfileLow = mock(EncoderProfiles.class); + EncoderProfiles mockProfile = mock(EncoderProfiles.class); + EncoderProfiles.VideoProfile mockVideoProfile = mock(EncoderProfiles.VideoProfile.class); + List mockVideoProfilesList = List.of(mockVideoProfile); + + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_LOW)) .thenReturn(mockProfileLow); + + when(mockProfile.getVideoProfiles()).thenReturn(mockVideoProfilesList); + when(mockVideoProfile.getHeight()).thenReturn(100); + when(mockVideoProfile.getWidth()).thenReturn(100); } @After @@ -116,6 +158,39 @@ public void checkIsSupport_returnsTrue() { assertTrue(resolutionFeature.checkIsSupported()); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroughLegacy() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLowLegacy, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + 1, ResolutionPreset.max)); + } + + @Config(minSdk = 31) @Test public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { mockedStaticProfile @@ -146,45 +221,112 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroug 1, ResolutionPreset.max)); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMaxLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) + @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } + @Config(minSdk = 31) @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { + before(); ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") @Test - public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMediumLegacy() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); } + @Config(minSdk = 31) @Test - public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLowLegacy() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); + } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 58f17cb758bf..82449a10188a 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 @@ -36,6 +36,7 @@ public class DeviceOrientationManagerTest { private DeviceOrientationManager deviceOrientationManager; @Before + @SuppressWarnings("deprecation") public void before() { mockActivity = mock(Activity.class); mockDartMessenger = mock(DartMessenger.class); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 5425409c2f3a..6cc58ee823d9 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -8,23 +8,42 @@ import static org.mockito.Mockito.*; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.MediaRecorder; import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.List; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InOrder; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) public class MediaRecorderBuilderTest { + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") @Test - public void ctor_test() { + public void ctor_testLegacy() { MediaRecorderBuilder builder = new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), ""); assertNotNull(builder); } + @Config(minSdk = 31) @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + public void ctor_test() { + MediaRecorderBuilder builder = + new MediaRecorderBuilder(CamcorderProfile.getAll("0", CamcorderProfile.QUALITY_1080P), ""); + + assertNotNull(builder); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabledLegacy() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -54,8 +73,67 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOEx inOrder.verify(recorder).prepare(); } + @Config(minSdk = 31) @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Config(minSdk = 31) + @Test(expected = IndexOutOfBoundsException.class) + public void build_shouldThrowExceptionWithoutVideoOrAudioProfiles() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabledLegacy() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -89,6 +167,49 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOExc inOrder.verify(recorder).prepare(); } + @Config(minSdk = 31) + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + EncoderProfiles.AudioProfile audioProfile = mockAudioProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setAudioEncoder(audioProfile.getCodec()); + inOrder.verify(recorder).setAudioEncodingBitRate(audioProfile.getBitrate()); + inOrder.verify(recorder).setAudioSamplingRate(audioProfile.getSampleRate()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + private CamcorderProfile getEmptyCamcorderProfile() { try { Constructor constructor = diff --git a/packages/camera/camera/android/src/test/resources/robolectric.properties b/packages/camera/camera/android/src/test/resources/robolectric.properties new file mode 100644 index 000000000000..90fbd74370a7 --- /dev/null +++ b/packages/camera/camera/android/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=30 \ No newline at end of file diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..476d65373723 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index a6738207fd15..b253d8e5f746 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=false android.enableR8=true From 0c23f8f5232dd3db9030659454ce0fa31f716c67 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 1 Nov 2021 09:37:20 +0100 Subject: [PATCH 132/259] [webview_flutter] Update webview packages for Android and iOS to implement `runJavascript` and `runJavascriptReturningResult`. (#4402) * [webview_flutter_platform_interface] Update webview platform interface with new methods for running JavaScript * Fix tests * [webview_flutter] Implemented `runJavaScript` and `runJavaScriptForResult` in iOS and Android packages. * Remove accidental development team inclusion from project.pbxproj * Implemented PR feedback * Updated runJavaScriptForResult behaviour * Implemented PR feedback partially * Implement PR feedback * Update changelog * Implement PR feedback from interface PR * Update changelog * Revert inclusion of development team * Implement PR feedback * Implemented platform interface PR feedback * Update pubspec dependency * Fixed capitalisation * Fixed capitalisation * Fix warning * Partially implement PR feedback * Partially implement PR feedback * Format * Update podfile * Update podfile * Update podfiles * Update iOS project files * Update podspec * Remove unnecessary podfile configuration * Implemented PR feedback * Format * Revert podfile changes * Implemented PR feedback * Fix formatting * Fix formatting * Fixed test. * Re-add integration tests for deprecated evaluateJavascript method. * Fix merge conflicts --- .../ios/Runner.xcodeproj/project.pbxproj | 97 ++++---- .../webview_flutter_android/CHANGELOG.md | 4 + .../android/build.gradle | 4 + .../webviewflutter/FlutterWebView.java | 15 +- .../webviewflutter/FlutterWebViewTest.java | 109 ++++++++- .../webview_flutter_test.dart | 90 +++++--- .../example/lib/main.dart | 10 +- .../example/lib/web_view.dart | 57 +++-- .../webview_flutter_android/pubspec.yaml | 2 +- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../webview_flutter_test.dart | 73 +++--- .../ios/Runner.xcodeproj/project.pbxproj | 103 +++++---- .../example/ios/RunnerTests/FLTWebViewTests.m | 213 ++++++++++++++++++ .../example/lib/main.dart | 10 +- .../example/lib/web_view.dart | 54 +++-- .../ios/Classes/FlutterWebView.h | 20 +- .../ios/Classes/FlutterWebView.m | 39 ++++ .../ios/webview_flutter_wkwebview.podspec | 2 +- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 19 files changed, 690 insertions(+), 218 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 62428d041adf..0759b31a2f25 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,13 +11,13 @@ 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 */; }; + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BEC8CD326B252E47ABE6C037 /* 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 */; }; - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0ABA59F25635F077C9EA161 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,11 +52,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 11DF059E983DF25F078B44CC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 4D2B3F45D8E6CA81EA52591E /* 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 = ""; }; + 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -64,7 +66,6 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -73,9 +74,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 = ""; }; - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +108,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */, + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,8 +146,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + EA36D6F90B795550E32A139A /* Pods */, + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +185,16 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + EA36D6F90B795550E32A139A /* Pods */ = { isa = PBXGroup; children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + 4D2B3F45D8E6CA81EA52591E /* Pods-Runner.debug.xcconfig */, + 11DF059E983DF25F078B44CC /* Pods-Runner.release.xcconfig */, + 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */, + 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + EA0C9BB56C9A98B4F095051B /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 1B3EA6BF26F6D525A8503093 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -338,41 +339,41 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 1B3EA6BF26F6D525A8503093 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -388,18 +389,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + EA0C9BB56C9A98B4F095051B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -473,7 +478,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +492,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index c0837263509d..176028f2b11c 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + ## 2.1.0 * Add `zoomEnabled` functionality. diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index ef1485a765d0..bb9fe9708eaa 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -58,4 +58,8 @@ android { } } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index b2a453adcdac..ed14107220b8 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -245,7 +245,11 @@ public void onMethodCall(MethodCall methodCall, Result result) { currentUrl(result); break; case "evaluateJavascript": - evaluateJavaScript(methodCall, result); + case "runJavascriptReturningResult": + evaluateJavaScript(methodCall, result, true); + break; + case "runJavascript": + evaluateJavaScript(methodCall, result, false); break; case "addJavascriptChannels": addJavaScriptChannels(methodCall, result); @@ -326,7 +330,8 @@ private void updateSettings(MethodCall methodCall, Result result) { } @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { + private void evaluateJavaScript( + MethodCall methodCall, final Result result, final boolean returnValue) { String jsString = (String) methodCall.arguments; if (jsString == null) { throw new UnsupportedOperationException("JavaScript string cannot be null"); @@ -336,7 +341,11 @@ private void evaluateJavaScript(MethodCall methodCall, final Result result) { new android.webkit.ValueCallback() { @Override public void onReceiveValue(String value) { - result.success(value); + if (returnValue) { + result.success(value); + } else { + result.success(null); + } } }); } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java index fd79bccabbce..f26a0ea6b9cc 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -6,31 +6,46 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebView; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; public class FlutterWebViewTest { private WebChromeClient mockWebChromeClient; private DownloadListener mockDownloadListener; private WebViewBuilder mockWebViewBuilder; private WebView mockWebView; + private MethodChannel.Result mockResult; + private Context mockContext; + private MethodChannel mockMethodChannel; @Before public void before() { + mockWebChromeClient = mock(WebChromeClient.class); mockWebViewBuilder = mock(WebViewBuilder.class); mockWebView = mock(WebView.class); mockDownloadListener = mock(DownloadListener.class); + mockResult = mock(MethodChannel.Result.class); + mockContext = mock(Context.class); + mockMethodChannel = mock(MethodChannel.class); when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) @@ -42,12 +57,11 @@ public void before() { .thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.build()).thenReturn(mockWebView); } @Test - public void createWebView_should_create_webview_with_default_configuration() { + public void createWebView_shouldCreateWebViewWithDefaultConfiguration() { FlutterWebView.createWebView( mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); @@ -59,6 +73,97 @@ public void createWebView_should_create_webview_with_default_configuration() { verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true); } + @Test(expected = UnsupportedOperationException.class) + public void evaluateJavaScript_shouldThrowForNullString() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + new MockedStatic.Verification() { + @Override + public void apply() throws Throwable { + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any()); + } + }) + .thenReturn(mockWebView); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall(new MethodCall("runJavascript", null), mockResult); + } + } + + @Test + public void evaluateJavaScript_shouldReturnValueOnSuccessForReturnValue() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + () -> + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any())) + .thenReturn(mockWebView); + doAnswer( + invocation -> { + android.webkit.ValueCallback callback = invocation.getArgument(1); + callback.onReceiveValue("Test JavaScript Result"); + return null; + }) + .when(mockWebView) + .evaluateJavascript(eq("Test JavaScript String"), any()); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall( + new MethodCall("runJavascriptReturningResult", "Test JavaScript String"), mockResult); + + // Verify + verify(mockResult, times(1)).success("Test JavaScript Result"); + } + } + + @Test + public void evaluateJavaScript_shouldReturnNilOnSuccessForNoReturnValue() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + () -> + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any())) + .thenReturn(mockWebView); + doAnswer( + invocation -> { + android.webkit.ValueCallback callback = invocation.getArgument(1); + callback.onReceiveValue("Test JavaScript Result"); + return null; + }) + .when(mockWebView) + .evaluateJavascript(eq("Test JavaScript String"), any()); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall( + new MethodCall("runJavascript", "Test JavaScript String"), mockResult); + + // Verify + verify(mockResult, times(1)).success(isNull()); + } + } + private Map createParameterMap(boolean usesHybridComposition) { Map params = new HashMap<>(); params.put("usesHybridComposition", usesHybridComposition); 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 c57d2bd55580..8e3dcb458ee8 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 @@ -76,6 +76,27 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = @@ -114,12 +135,12 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -159,11 +180,7 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }, skip: _skipDueToIssue86757); @@ -410,7 +427,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -439,7 +457,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -470,7 +488,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -498,7 +517,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -548,7 +567,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); }); @@ -614,7 +633,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -648,7 +668,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -684,7 +704,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -717,7 +738,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -982,15 +1003,16 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( + final String viewportRectJSON = await _runJavaScriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = jsonDecode(viewportRectJSON); // Check that the input is originally outside of the viewport. - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String initialInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = jsonDecode(initialInputClientRectJSON); @@ -999,12 +1021,13 @@ void main() { viewportRectRelativeToViewport['bottom'], isFalse); - await controller.evaluateJavascript('inputEl.focus()'); + await controller.runJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String lastInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = jsonDecode(lastInputClientRectJSON); @@ -1060,7 +1083,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1186,7 +1209,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1226,7 +1249,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1281,7 +1304,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1317,7 +1340,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1331,7 +1354,7 @@ void main() { ); testWidgets( - 'javascript does not run in parent window', + 'JavaScript does not run in parent window', (WidgetTester tester) async { final String iframe = ''' @@ -1388,9 +1411,10 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect(controller.runJavascriptReturningResult('iframeLoaded'), + completion('true')); expect( - controller.evaluateJavascript( + controller.runJavascriptReturningResult( 'document.querySelector("p") && document.querySelector("p").textContent'), completion('null'), ); @@ -1409,10 +1433,10 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { - return _evaluateJavascript(controller, 'navigator.userAgent;'); + return _runJavaScriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavaScriptReturningResult( WebViewController controller, String js) async { - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 65f49716aaac..a22d165a6ff3 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -195,14 +195,14 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Snackbar JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(SnackBar( content: Column( @@ -217,7 +217,7 @@ class _SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(const SnackBar( @@ -226,7 +226,7 @@ class _SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } @@ -340,5 +340,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. +/// Callback type for handling messages sent from JavaScript running in a web view. typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 5e8a2798b8ca..b43507447462 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -116,7 +116,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -126,7 +126,7 @@ class WebView extends StatefulWidget { /// The JavaScript code can then call `postMessage` on that object to send a message that will be /// passed to [JavascriptChannel.onMessageReceived]. /// - /// For example for the following JavascriptChannel: + /// For example for the following [JavascriptChannel]: /// /// ```dart /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); @@ -189,7 +189,7 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. final PageFinishedCallback? onPageFinished; @@ -487,31 +487,46 @@ class WebViewController { _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); } - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// Returns the evaluation result as a JSON formatted string. + /// The Future completes with an error if a JavaScript error occurred. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 577d37359af1..ac208a09ebc0 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.0 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index b75519af476c..4db6dbfd2864 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + ## 2.1.0 * Add `zoomEnabled` functionality. 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 8ba17a2428c5..17e896c94888 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 @@ -73,6 +73,27 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -110,11 +131,11 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }); - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -154,11 +175,7 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }); @@ -404,7 +421,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -433,7 +451,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -464,7 +482,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -492,7 +511,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -542,7 +561,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -593,7 +612,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }); }); @@ -659,7 +678,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -693,7 +713,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -729,7 +749,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -762,7 +783,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -919,7 +940,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1050,7 +1071,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1090,7 +1111,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1145,7 +1166,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1179,7 +1200,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1204,13 +1225,13 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { - return _evaluateJavascript(controller, 'navigator.userAgent;'); + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavascriptReturningResult( WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); + return await controller.runJavascriptReturningResult(js); } - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } 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 62428d041adf..ba0deb4781d4 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 @@ -16,8 +16,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */; }; + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 572FFC2B2BA326B420B22679 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,11 +52,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -64,7 +65,6 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -73,12 +73,12 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7A1921261392D1CBDAEC2E8 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +108,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 52FBC2B567345431F81A0A0F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */, + 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,8 +146,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + B8AEEA11D6ECBD09750349AE /* Pods */, + 52FBC2B567345431F81A0A0F /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +185,16 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { + B8AEEA11D6ECBD09750349AE /* Pods */ = { isa = PBXGroup; children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { - isa = PBXGroup; - children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */, + B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */, + C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, + 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -338,21 +339,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; - }; - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -374,30 +361,34 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + 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"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); @@ -406,6 +397,20 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -473,7 +478,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +492,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index 631c4a105063..9d127c2c4aaa 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -88,4 +88,217 @@ - (void)testContentInsetsSumAlwaysZeroAfterSetFrame { } } +- (void)testRunJavascriptFailsForNullString { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:nil] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptRunsStringWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(@"RESULT", nil); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturnsErrorResultForWKError { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + NSError *testError = + [NSError errorWithDomain:@"" + // Any error code but WKErrorJavascriptResultTypeIsUnsupported + code:WKErrorJavaScriptResultTypeIsUnsupported + 1 + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturnsSuccessForWKErrorJavascriptResultTypeIsUnsupported { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return nil result over the method channel."]; + NSError *testError = [NSError errorWithDomain:@"" + code:WKErrorJavaScriptResultTypeIsUnsupported + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultFailsForNullString { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:nil] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultRunsStringWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(@"RESULT", nil); + }]; + controller.webView = mockView; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([@"RESULT" isEqualToString:result]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultReturnsErrorResultForWKError { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + NSError *testError = [NSError errorWithDomain:@"" + code:5 + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + @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 15b4cfc7c549..21240f63ec1a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -199,14 +199,14 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Snackbar JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, @@ -220,7 +220,7 @@ class _SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), @@ -228,7 +228,7 @@ class _SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } @@ -340,5 +340,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. +/// Callback type for handling messages sent from JavaScript running in a web view. typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index d57499dcef50..403db1f08ac6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -94,7 +94,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -167,7 +167,7 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. final PageFinishedCallback? onPageFinished; @@ -416,31 +416,51 @@ class WebViewController { _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); } - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// On iOS depending on the value type the return value would be one of: + /// When running JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. /// + /// Depending on the value type the return value would be one of: /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// The Future completes with an error if a JavaScript error occurred, or if the + /// type the given expression evaluates to is unsupported. Unsupported values include + /// certain non primitive types, as well as `undefined` or `null` on iOS 14+. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 6e795f7d1528..db52124d6a7c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -7,26 +7,30 @@ NS_ASSUME_NONNULL_BEGIN +/** + * The WkWebView used for the plugin. + * + * This class overrides some methods in `WKWebView` to serve the needs for the plugin. + */ +@interface FLTWKWebView : WKWebView +@end + @interface FLTWebViewController : NSObject +@property(nonatomic) FLTWKWebView* webView; + - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject*)messenger; - (UIView*)view; + +- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; @end @interface FLTWebViewFactory : NSObject - (instancetype)initWithMessenger:(NSObject*)messenger; @end -/** - * The WkWebView used for the plugin. - * - * This class overrides some methods in `WKWebView` to serve the needs for the plugin. - */ -@interface FLTWKWebView : WKWebView -@end - NS_ASSUME_NONNULL_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 7fd78ee302d0..5e12f8acb2ea 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -151,6 +151,10 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onCurrentUrl:call result:result]; } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { [self onEvaluateJavaScript:call result:result]; + } else if ([[call method] isEqualToString:@"runJavascript"]) { + [self onRunJavaScript:call result:result sendReturnValue:NO]; + } else if ([[call method] isEqualToString:@"runJavascriptReturningResult"]) { + [self onRunJavaScript:call result:result sendReturnValue:YES]; } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { [self onAddJavaScriptChannels:call result:result]; } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { @@ -244,6 +248,41 @@ - (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)resu }]; } +- (void)onRunJavaScript:(FlutterMethodCall*)call + result:(FlutterResult)result + sendReturnValue:(BOOL)sendReturnValue { + NSString* jsString = [call arguments]; + if (!jsString) { + result([FlutterError errorWithCode:@"runJavascript_failed" + message:@"JavaScript String cannot be null" + details:nil]); + return; + } + [_webView + evaluateJavaScript:jsString + completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { + if (error) { + // WebKit will throw an error (WKErrorJavaScriptResultTypeIsUnsupported) 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. + BOOL sendError = + sendReturnValue || error.code != WKErrorJavaScriptResultTypeIsUnsupported; + result(sendError + ? [FlutterError + errorWithCode:(sendReturnValue ? @"runJavascriptReturningResult_failed" + : @"runJavascript_failed") + message:@"Failed running JavaScript" + details:[NSString + stringWithFormat:@"JavaScript string was: '%@'\n%@", + jsString, error]] + : nil); + return; + } + result(sendReturnValue ? [NSString stringWithFormat:@"%@", evaluateResult] : nil); + }]; +} + - (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { NSArray* channelNames = [call arguments]; NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec index 37905f147489..2dfb336df35a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -18,6 +18,6 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index bfa4b8050e90..5176adb9749c 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.0 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" From 16e410bdf0321ff137443111d14e5e565822eff8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 1 Nov 2021 16:35:52 -0400 Subject: [PATCH 133/259] Remove -Werror from deprecated plugin Android builds (#4466) Now that the default Flutter app template uses a newer compile SDK on Android, the build_all test (which generates an application) is subject to new out-of-band deprecation warnings. For plugins with `-Werror` specified, these are errors, which break CI. To prevent this, remove `-Werror` from the Android builds of all the deprecated plugins that used it. --- packages/android_alarm_manager/CHANGELOG.md | 1 + packages/android_alarm_manager/android/build.gradle | 2 +- packages/android_intent/CHANGELOG.md | 1 + packages/android_intent/android/build.gradle | 2 +- packages/connectivity/connectivity/CHANGELOG.md | 1 + packages/connectivity/connectivity/android/build.gradle | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index d53b932e3f0f..2c067a31326a 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove support for the V1 Android embedding. * Updated Android lint settings. +* Removed `-Werror` in Android builds. ## 2.0.2 diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index 7712ed56fe6f..9236ffbdd086 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -1,6 +1,6 @@ group 'io.flutter.plugins.androidalarmmanager' version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] +def args = ["-Xlint:deprecation","-Xlint:unchecked"] buildscript { repositories { diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 79eafe70e821..c30b4839f4c6 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -3,6 +3,7 @@ * Remove references to the V1 Android embedding. * Updated Android lint settings. * Specify Java 8 for Android build. +* Removed `-Werror` in Android builds. ## 2.0.2 diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index f0af1602dbb1..4bc8e85c40bc 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -1,6 +1,6 @@ group 'io.flutter.plugins.androidintent' version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] +def args = ["-Xlint:deprecation","-Xlint:unchecked"] buildscript { repositories { diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 932565842efd..81d2eff08006 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -3,6 +3,7 @@ * Remove references to the Android V1 embedding. * Updated Android lint settings. * Specify Java 8 for Android build. +* Removed `-Werror` in Android builds. ## 3.0.6 diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index e1ba0c7c892e..33c63fcb5619 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -1,6 +1,6 @@ group 'io.flutter.plugins.connectivity' version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] +def args = ["-Xlint:deprecation","-Xlint:unchecked"] buildscript { repositories { From 2c16458787cc72b0a790526d3344944136835685 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 2 Nov 2021 09:53:02 +0100 Subject: [PATCH 134/259] [webview_flutter] Add platform interface method `loadRequest`. (#4450) --- .../CHANGELOG.md | 4 ++ .../webview_method_channel.dart | 8 +++ .../webview_platform_controller.dart | 19 ++++++ .../lib/src/types/types.dart | 1 + .../lib/src/types/webview_request.dart | 58 +++++++++++++++++++ .../pubspec.yaml | 2 +- .../webview_method_channel_test.dart | 52 +++++++++++++++++ .../test/src/types/webview_request_test.dart | 39 +++++++++++++ 8 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 04641f97dc79..0579121be82b 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.0 + +* Added `loadRequest` method to platform interface. + ## 1.2.0 * Added `runJavascript` and `runJavascriptReturningResult` interface methods to supersede `evaluateJavascript`. 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 9610038eec82..a88479e7e024 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 @@ -91,6 +91,14 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { }); } + @override + Future loadRequest(WebViewRequest request) async { + assert(request != null); + return _channel.invokeMethod('loadRequest', { + 'request': request.toJson(), + }); + } + @override Future currentUrl() => _channel.invokeMethod('currentUrl'); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart index b42da4326079..806f9500dbad 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -39,6 +39,25 @@ abstract class WebViewPlatformController { "WebView loadUrl is not implemented on the current platform"); } + /// Makes a specific HTTP request ands loads the response in the webview. + /// + /// [WebViewRequest.method] must be one of the supported HTTP methods + /// in [WebViewRequestMethod]. + /// + /// If [WebViewRequest.headers] is not empty, its key-value pairs will be + /// added as the headers for the request. + /// + /// If [WebViewRequest.body] is not null, it will be added as the body + /// for the request. + /// + /// Throws an ArgumentError if [WebViewRequest.uri] has empty scheme. + Future loadRequest( + WebViewRequest request, + ) { + throw UnimplementedError( + "WebView loadRequest is not implemented on the current platform"); + } + /// Updates the webview settings. /// /// Any non null field in `settings` will be set as the new setting value. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart index b1a9b9b9daa8..8ce834196cd8 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -10,3 +10,4 @@ export 'javascript_mode.dart'; export 'web_resource_error.dart'; export 'web_resource_error_type.dart'; export 'web_settings.dart'; +export 'webview_request.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart new file mode 100644 index 000000000000..5e520f1baa9e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +/// Defines the supported HTTP methods for loading a page in [WebView]. +enum WebViewRequestMethod { + /// HTTP GET method. + get, + + /// HTTP POST method. + post, +} + +/// Extension methods on the [WebViewRequestMethod] enum. +extension WebViewRequestMethodExtensions on WebViewRequestMethod { + /// Converts [WebViewRequestMethod] to [String] format. + String serialize() { + switch (this) { + case WebViewRequestMethod.get: + return 'get'; + case WebViewRequestMethod.post: + return 'post'; + } + } +} + +/// Defines the parameters that can be used to load a page in the [WebView]. +class WebViewRequest { + /// Creates the [WebViewRequest]. + WebViewRequest({ + required this.uri, + required this.method, + this.headers = const {}, + this.body, + }); + + /// URI for the request. + final Uri uri; + + /// HTTP method used to make the request. + final WebViewRequestMethod method; + + /// Headers for the request. + final Map headers; + + /// HTTP body for the request. + final Uint8List? body; + + /// Serializes the [WebViewRequest] to JSON. + Map toJson() => { + 'uri': this.uri.toString(), + 'method': this.method.serialize(), + 'headers': this.headers, + 'body': this.body, + }; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 994c3dcdebdf..508af0ef0862 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/webview_flut issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.2.0 +version: 1.3.0 environment: sdk: ">=2.12.0 <3.0.0" 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 85f184f9f715..b85b7b3df286 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 @@ -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:typed_data'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -101,6 +103,56 @@ void main() { ); }); + test('loadRequest', () async { + await webViewPlatform.loadRequest(WebViewRequest( + uri: Uri.parse('https://test.url'), + method: WebViewRequestMethod.get, + )); + + expect( + log, + [ + isMethodCall( + 'loadRequest', + arguments: { + 'request': { + 'uri': 'https://test.url', + 'method': 'get', + 'headers': {}, + 'body': null, + } + }, + ), + ], + ); + }); + + test('loadRequest with optional parameters', () async { + await webViewPlatform.loadRequest(WebViewRequest( + uri: Uri.parse('https://test.url'), + method: WebViewRequestMethod.get, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('hello world'.codeUnits), + )); + + expect( + log, + [ + isMethodCall( + 'loadRequest', + arguments: { + 'request': { + 'uri': 'https://test.url', + 'method': 'get', + 'headers': {'foo': 'bar'}, + 'body': 'hello world'.codeUnits, + } + }, + ), + ], + ); + }); + test('currentUrl', () async { final String? currentUrl = await webViewPlatform.currentUrl(); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart new file mode 100644 index 000000000000..5d2b568fe5dd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_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 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; + +void main() { + test('WebViewRequestMethod should serialize correctly', () { + expect(WebViewRequestMethod.get.serialize(), 'get'); + expect(WebViewRequestMethod.post.serialize(), 'post'); + }); + + test('WebViewRequest should serialize correctly', () { + WebViewRequest request; + Map serializedRequest; + // Test serialization without headers or a body + request = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.get, + ); + serializedRequest = request.toJson(); + expect(serializedRequest['uri'], 'https://flutter.dev'); + expect(serializedRequest['method'], 'get'); + expect(serializedRequest['headers'], {}); + expect(serializedRequest['body'], null); + // Test serialization of headers and body + request = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.get, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('Example Body'.codeUnits), + ); + serializedRequest = request.toJson(); + expect(serializedRequest['headers'], {'foo': 'bar'}); + expect(serializedRequest['body'], 'Example Body'.codeUnits); + }); +} From db098ed8408de3d2055e54d8e37bf18ffa790c26 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Tue, 2 Nov 2021 14:31:43 -0700 Subject: [PATCH 135/259] [ci] update build_runner dep in android_intent and file_selector example (#4467) --- packages/android_intent/pubspec.yaml | 2 +- packages/file_selector/file_selector_web/example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 793f82d4762d..59b1159b3835 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -26,4 +26,4 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.10.0 - build_runner: ^1.11.1 + build_runner: ^2.0.0 diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index dd98c28d1a99..c5ffadaef071 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^1.10.0 + build_runner: ^2.0.0 file_selector_web: path: ../ flutter_driver: From 53fff22979d2def459aab7a773015a50630c17ff Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Tue, 2 Nov 2021 18:58:04 -0700 Subject: [PATCH 136/259] [ci] Remove unused dep from file_selector. (#4468) --- packages/file_selector/file_selector_web/example/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index c5ffadaef071..8998587615e5 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -10,7 +10,6 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^2.0.0 file_selector_web: path: ../ flutter_driver: From 2e102b8a8c41ab199be63b3888cad83004429606 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Wed, 3 Nov 2021 09:33:22 -0700 Subject: [PATCH 137/259] [ci] add pedantic dep to new in_app_purchase pkgs (#4471) --- packages/in_app_purchase/in_app_purchase_android/pubspec.yaml | 1 + packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 1 + 2 files changed, 2 insertions(+) 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 7230b56d5af9..fef2dab2ff74 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -29,4 +29,5 @@ dev_dependencies: flutter_test: sdk: flutter json_serializable: ^6.0.0 + pedantic: ^1.10.0 test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index b40ad1da149e..80ef921cada5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -28,4 +28,5 @@ dev_dependencies: flutter_test: sdk: flutter json_serializable: ^6.0.0 + pedantic: ^1.10.0 test: ^1.16.0 From e39a584ab08ed432856301278d03c84df241adc3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 4 Nov 2021 16:04:09 +0100 Subject: [PATCH 138/259] [webview_flutter] Add interface methods to load local files and HTML strings. (#4446) * Add methods to load HTML and Flutter assets. Adds methods to the webview_flutter_platform_interface to support loading content from Flutter asset defined in the pubspec.yaml of directly from HTML string. * Renamed loadHtml to loadHtmlString * Support loading arbitrary files instead of only Flutter assets * Update changelog description * Fix formatting * Fix formatting --- .../CHANGELOG.md | 4 ++ .../webview_method_channel.dart | 18 ++++++ .../webview_platform_controller.dart | 26 +++++++++ .../pubspec.yaml | 2 +- .../webview_method_channel_test.dart | 55 +++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 0579121be82b..4c7434a86b41 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.4.0 + +* Added `loadFile` and `loadHtml` interface methods. + ## 1.3.0 * Added `loadRequest` method to platform interface. 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 a88479e7e024..8df9f4c62b33 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 @@ -79,6 +79,24 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { ); } + @override + Future loadFile(String absoluteFilePath) async { + assert(absoluteFilePath != null); + return _channel.invokeMethod('loadFile', absoluteFilePath); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) async { + assert(html != null); + return _channel.invokeMethod('loadHtmlString', { + 'html': html, + 'baseUrl': baseUrl, + }); + } + @override Future loadUrl( String url, diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart index 806f9500dbad..cfc817450372 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -23,6 +23,32 @@ abstract class WebViewPlatformController { /// The `handler` parameter must not be null. WebViewPlatformController(WebViewPlatformCallbacksHandler handler); + /// Loads the file located on the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws an ArgumentError if the [absoluteFilePath] does not exist. + Future loadFile( + String absoluteFilePath, + ) { + throw UnimplementedError( + "WebView loadFlutterAsset is not implemented on the current platform"); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + throw UnimplementedError( + "WebView loadHtmlString is not implemented on the current platform"); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 508af0ef0862..4a4746d8ab68 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/webview_flut 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.3.0 +version: 1.4.0 environment: sdk: ">=2.12.0 <3.0.0" 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 b85b7b3df286..396013535aa9 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 @@ -57,6 +57,61 @@ void main() { log.clear(); }); + test('loadFile', () async { + await webViewPlatform.loadFile( + '/folder/asset.html', + ); + + expect( + log, + [ + isMethodCall( + 'loadFile', + arguments: '/folder/asset.html', + ), + ], + ); + }); + + test('loadHtmlString without base URL', () async { + await webViewPlatform.loadHtmlString( + 'Test HTML string', + ); + + expect( + log, + [ + isMethodCall( + 'loadHtmlString', + arguments: { + 'html': 'Test HTML string', + 'baseUrl': null, + }, + ), + ], + ); + }); + + test('loadHtmlString without base URL', () async { + await webViewPlatform.loadHtmlString( + 'Test HTML string', + baseUrl: 'https://flutter.dev', + ); + + expect( + log, + [ + isMethodCall( + 'loadHtmlString', + arguments: { + 'html': 'Test HTML string', + 'baseUrl': 'https://flutter.dev', + }, + ), + ], + ); + }); + test('loadUrl with headers', () async { await webViewPlatform.loadUrl( 'https://test.url', From d6ca8a368ce3739f0b0d02f9df28cc8eb69a0943 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Thu, 4 Nov 2021 18:33:04 +0100 Subject: [PATCH 139/259] [webview_flutter] Deprecate evaluateJavascript in favour of runJavaScript and runJavaScriptForResult. (#4403) --- .../webview_flutter/CHANGELOG.md | 5 + .../webview_flutter_test.dart | 95 ++++++++++++------- .../webview_flutter/example/lib/main.dart | 8 +- .../webview_flutter/lib/src/webview.dart | 74 ++++++++++++--- .../webview_flutter/pubspec.yaml | 8 +- .../test/webview_flutter_test.dart | 80 +++++++++++++++- 6 files changed, 213 insertions(+), 57 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index f599889c0d5e..b499cbab6f8d 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.0 + +* Added `runJavascript` and `runJavascriptForResult` to supersede `evaluateJavascript`. +* Deprecated `evaluateJavascript`. + ## 2.1.2 * Fix typos in the README. 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 3379bafa2346..9279f31a1e3f 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 @@ -70,6 +70,28 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = @@ -108,12 +130,12 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }, skip: Platform.isAndroid && _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -153,11 +175,7 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }, skip: Platform.isAndroid && _skipDueToIssue86757); @@ -404,7 +422,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -433,7 +452,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -464,7 +483,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -492,7 +512,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -542,7 +562,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -594,7 +614,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }, skip: Platform.isAndroid); }); @@ -660,7 +680,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -694,7 +715,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -730,7 +751,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -763,7 +785,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -1028,15 +1050,16 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( + final String viewportRectJSON = await _runJavascriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = jsonDecode(viewportRectJSON); // Check that the input is originally outside of the viewport. - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String initialInputClientRectJSON = + await _runJavascriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = jsonDecode(initialInputClientRectJSON); @@ -1045,12 +1068,13 @@ void main() { viewportRectRelativeToViewport['bottom'], isFalse); - await controller.evaluateJavascript('inputEl.focus()'); + await controller.runJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String lastInputClientRectJSON = + await _runJavascriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = jsonDecode(lastInputClientRectJSON); @@ -1106,7 +1130,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1237,7 +1261,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1277,7 +1301,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1332,7 +1356,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1368,7 +1392,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1382,7 +1406,7 @@ void main() { ); testWidgets( - 'javascript does not run in parent window', + 'JavaScript does not run in parent window', (WidgetTester tester) async { final String iframe = ''' @@ -1439,9 +1463,10 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect(controller.runJavascriptReturningResult('iframeLoaded'), + completion('true')); expect( - controller.evaluateJavascript( + controller.runJavascriptReturningResult( 'document.querySelector("p") && document.querySelector("p").textContent'), completion('null'), ); @@ -1461,13 +1486,13 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { - return _evaluateJavascript(controller, 'navigator.userAgent;'); + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavascriptReturningResult( WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); + return await controller.runJavascriptReturningResult(js); } - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index c456a9691455..2fd2087378ba 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -210,14 +210,14 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(SnackBar( content: Column( @@ -232,7 +232,7 @@ class SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(const SnackBar( @@ -241,7 +241,7 @@ class SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 7699cc46c5d3..410a99599d4f 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -148,7 +148,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -221,9 +221,9 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. + /// [WebViewController.runJavascript] or [WebViewController.runJavascriptReturningResult] can assume this. final PageFinishedCallback? onPageFinished; /// Invoked when a page is loading. @@ -594,27 +594,75 @@ class WebViewController { /// /// On iOS depending on the value type the return value would be one of: /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete + /// the Future with an error. /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// The Future completes with an error if a JavaScript error occurred, + /// or on iOS, if the type of the evaluated expression is + /// not supported as described above. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. + @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]') Future evaluateJavascript(String javascriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method return _webViewPlatformController.evaluateJavascript(javascriptString); } + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, + /// and returns the result. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// + /// The Future completes with an error if a JavaScript error occurred, + /// or if the type the given expression evaluates to is unsupported. + /// Unsupported values include certain non primitive types on iOS, as well as + /// `undefined` or `null` on iOS 14+. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait + /// for the [WebView.onPageFinished] callback. This guarantees all the + /// JavaScript embedded in the main frame HTML has been loaded. + Future runJavascriptReturningResult(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); + } + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); + } + /// Returns the title of the currently loaded page. Future getTitle() { return _webViewPlatformController.getTitle(); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index dabfe0d0d14a..95a86fa018ea 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.2 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.0.0 - webview_flutter_android: ^2.0.13 - webview_flutter_wkwebview: ^2.0.13 + webview_flutter_platform_interface: ^1.2.0 + webview_flutter_android: ^2.2.0 + webview_flutter_wkwebview: ^2.2.0 dev_dependencies: flutter_driver: diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index f7d09266a64b..8d0f754a529f 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -372,7 +372,9 @@ void main() { ), ); expect( - await controller.evaluateJavascript("fake js string"), "fake js string", + // ignore: deprecated_member_use_from_same_package + await controller.evaluateJavascript("fake js string"), + "fake js string", reason: 'should get the argument'); }); @@ -389,11 +391,80 @@ void main() { ), ); expect( + // ignore: deprecated_member_use_from_same_package () => controller.evaluateJavascript('fake js string'), throwsA(anything), ); }); + testWidgets('runJavaScript', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + await controller.runJavascript('fake js string'); + expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString, + 'fake js string'); + }); + + testWidgets('runJavaScript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(await controller.runJavascriptReturningResult("fake js string"), + "fake js string", + reason: 'should get the argument'); + }); + + testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascriptReturningResult('fake js string'), + throwsA(anything), + ); + }); + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { await tester.pumpWidget( const WebView( @@ -960,6 +1031,8 @@ class FakePlatformWebView { bool? debuggingEnabled; String? userAgent; + String? lastRunJavaScriptString; + Future onMethodCall(MethodCall call) { switch (call.method) { case 'loadUrl': @@ -993,8 +1066,13 @@ class FakePlatformWebView { return Future.sync(() {}); case 'currentUrl': return Future.value(currentUrl); + case 'runJavascriptReturningResult': case 'evaluateJavascript': + lastRunJavaScriptString = call.arguments; return Future.value(call.arguments); + case 'runJavascript': + lastRunJavaScriptString = call.arguments; + return Future.sync(() {}); case 'addJavascriptChannels': final List channelNames = List.from(call.arguments); javascriptChannelNames!.addAll(channelNames); From 3da76546d75d6f01396389ab30642c78e909df2a Mon Sep 17 00:00:00 2001 From: Nick Bradshaw Date: Fri, 5 Nov 2021 00:53:02 -0700 Subject: [PATCH 140/259] [webview_flutter]Add zoom to android webview (#3325) --- .../webview_flutter/webview_flutter/AUTHORS | 2 + .../webview_flutter/CHANGELOG.md | 4 ++ .../webview_flutter_test.dart | 2 +- .../webview_flutter/example/lib/main.dart | 1 + .../webview_flutter/lib/src/webview.dart | 14 +++++ .../webview_flutter/pubspec.yaml | 4 +- .../test/webview_flutter_test.dart | 55 ++++++++++++++++++- 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/AUTHORS b/packages/webview_flutter/webview_flutter/AUTHORS index 493a0b4ef9c2..85628e432f60 100644 --- a/packages/webview_flutter/webview_flutter/AUTHORS +++ b/packages/webview_flutter/webview_flutter/AUTHORS @@ -64,3 +64,5 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Nick Bradshaw +Antonino Di Natale diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index b499cbab6f8d..8f4826823c38 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Add ability to enable/disable zoom functionality. + ## 2.2.0 * Added `runJavascript` and `runJavascriptForResult` to supersede `evaluateJavascript`. 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 9279f31a1e3f..69ddcc685ff8 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 @@ -11,9 +11,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter/platform_interface.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 2fd2087378ba..b660ce37a0b1 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 410a99599d4f..442999d0ff5c 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -90,6 +90,7 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.zoomEnabled = true, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, @@ -268,6 +269,12 @@ class WebView extends StatefulWidget { /// By default `userAgent` is null. final String? userAgent; + /// A Boolean value indicating whether the WebView should support zooming + /// using its on-screen zoom controls and gestures. + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + /// Which restrictions apply on automatic media playback. /// /// This initial value is applied to the platform's webview upon creation. Any following @@ -359,6 +366,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, ); } @@ -374,12 +382,14 @@ WebSettings _clearUnchangedWebSettings( assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; WebSetting userAgent = WebSetting.absent(); + bool? zoomEnabled; if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -395,6 +405,9 @@ WebSettings _clearUnchangedWebSettings( if (currentValue.userAgent != newValue.userAgent) { userAgent = newValue.userAgent; } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } return WebSettings( javascriptMode: javascriptMode, @@ -402,6 +415,7 @@ WebSettings _clearUnchangedWebSettings( hasProgressTracking: hasProgressTracking, debuggingEnabled: debuggingEnabled, userAgent: userAgent, + zoomEnabled: zoomEnabled, ); } diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 95a86fa018ea..c7ac2e0200f9 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,8 +19,8 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.2.0 webview_flutter_android: ^2.2.0 + webview_flutter_platform_interface: ^1.2.0 webview_flutter_wkwebview: ^2.2.0 dev_dependencies: diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index 8d0f754a529f..e325f68dc0b7 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -10,8 +10,8 @@ import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; typedef void VoidCallback(); @@ -915,6 +915,50 @@ void main() { }); }); + group('zoomEnabled', () { + testWidgets('Enable zoom', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + zoomEnabled: true, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.zoomEnabled, isTrue); + }); + + testWidgets('defaults to true', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.zoomEnabled, isTrue); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + await tester.pumpWidget(WebView( + key: key, + zoomEnabled: true, + )); + + expect(platformWebView.zoomEnabled, isTrue); + + await tester.pumpWidget(WebView( + key: key, + zoomEnabled: false, + )); + + expect(platformWebView.zoomEnabled, isFalse); + }); + }); + group('Custom platform implementation', () { setUpAll(() { WebView.platform = MyWebViewPlatform(); @@ -944,6 +988,7 @@ void main() { debuggingEnabled: false, userAgent: WebSetting.of(null), gestureNavigationEnabled: true, + zoomEnabled: true, ), ))); }); @@ -1011,6 +1056,7 @@ class FakePlatformWebView { params['settings']['hasNavigationDelegate'] ?? false; debuggingEnabled = params['settings']['debuggingEnabled']; userAgent = params['settings']['userAgent']; + zoomEnabled = params['settings']['zoomEnabled'] ?? true; channel = MethodChannel( 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); channel.setMockMethodCallHandler(onMethodCall); @@ -1030,6 +1076,7 @@ class FakePlatformWebView { bool? hasNavigationDelegate; bool? debuggingEnabled; String? userAgent; + bool? zoomEnabled; String? lastRunJavaScriptString; @@ -1050,6 +1097,9 @@ class FakePlatformWebView { debuggingEnabled = call.arguments['debuggingEnabled']; } userAgent = call.arguments['userAgent']; + if (call.arguments['zoomEnabled'] != null) { + zoomEnabled = call.arguments['zoomEnabled']; + } break; case 'canGoBack': return Future.sync(() => currentPosition > 0); @@ -1307,7 +1357,8 @@ class MatchesWebSettings extends Matcher { _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && _webSettings!.gestureNavigationEnabled == webSettings.gestureNavigationEnabled && - _webSettings!.userAgent == webSettings.userAgent; + _webSettings!.userAgent == webSettings.userAgent && + _webSettings!.zoomEnabled == webSettings.zoomEnabled; } } From b401c84200bccd2b4747c2273ea5e13802dd569f Mon Sep 17 00:00:00 2001 From: Russell Wheatley Date: Fri, 5 Nov 2021 07:58:05 +0000 Subject: [PATCH 141/259] [video_player]: initial test coverage file() constructor api (#3407) --- .../integration_test/video_player_test.dart | 24 ++++++++++++++++++- .../video_player/example/pubspec.yaml | 3 ++- 2 files changed, 25 insertions(+), 2 deletions(-) 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 373538ad365e..866b5bce0a8d 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 @@ -3,11 +3,15 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:integration_test/integration_test.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:video_player/video_player.dart'; const Duration _playDuration = Duration(seconds: 1); @@ -224,5 +228,23 @@ void main() { skip: kIsWeb || // Web does not support local assets. // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 defaultTargetPlatform == TargetPlatform.iOS); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + String tempDir = (await getTemporaryDirectory()).path; + ByteData bytes = await rootBundle.load('assets/Butterfly-209.mp4'); + + File file = File('$tempDir/Butterfly-209.mp4'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + VideoPlayerController fileController = VideoPlayerController.file(file); + await fileController.initialize(); + + await fileController.play(); + expect(fileController.value.isPlaying, true); + + await fileController.pause(); + expect(fileController.value.isPlaying, false); + }, skip: kIsWeb); }); } diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 0539f3c6f56c..ce1787e2b427 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -24,8 +24,9 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - test: any + path_provider: ^2.0.6 pedantic: ^1.10.0 + test: any flutter: uses-material-design: true From 27eda487859c77ac9f78ea5c58acd2d29b457c36 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Fri, 5 Nov 2021 12:47:42 -0700 Subject: [PATCH 142/259] Fix NullPointerException (#4212) --- .../webview_flutter_android/CHANGELOG.md | 5 ++ .../webviewflutter/InputAwareWebView.java | 9 +++- .../webviewflutter/InputAwareWebViewTest.java | 48 +++++++++++++++++++ .../webview_flutter_android/pubspec.yaml | 2 +- 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 176028f2b11c..b241be63248c 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.1 + +* Fix `NullPointerException` from a race condition when changing focus. This only affects `WebView` +when it is created without Hybrid Composition. + ## 2.2.0 * Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java index 0a4cb3effd29..1276ac81acae 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -158,7 +158,7 @@ private void resetInputConnection() { *

{@code targetView} should have a {@link View#getHandler} method with the thread that future * InputConnections should be created on. */ - private void setInputConnectionTarget(final View targetView) { + void setInputConnectionTarget(final View targetView) { if (containerView == null) { Log.e( TAG, @@ -171,6 +171,13 @@ private void setInputConnectionTarget(final View targetView) { new Runnable() { @Override public void run() { + if (containerView == null) { + Log.e( + TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); // This is a hack to make InputMethodManager believe that the target view now has focus. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java new file mode 100644 index 000000000000..0eb078d5a7cf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.view.View; +import org.junit.Test; + +public class InputAwareWebViewTest { + static class TestView extends View { + Runnable postAction; + + public TestView(Context context) { + super(context); + } + + @Override + public boolean post(Runnable action) { + postAction = action; + return true; + } + } + + @Test + public void runnableChecksContainerViewIsNull() { + final Context mockContext = mock(Context.class); + + final TestView containerView = new TestView(mockContext); + final InputAwareWebView inputAwareWebView = new InputAwareWebView(mockContext, containerView); + + final View mockProxyAdapterView = mock(View.class); + + inputAwareWebView.setInputConnectionTarget(mockProxyAdapterView); + inputAwareWebView.setContainerView(null); + + assertNotNull(containerView.postAction); + containerView.postAction.run(); + verify(mockProxyAdapterView, never()).onWindowFocusChanged(anyBoolean()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index ac208a09ebc0..9f9998e28d58 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.2.0 +version: 2.2.1 environment: sdk: ">=2.14.0 <3.0.0" From 5bf7fb0360aa70571c46d5b0ac64949fc81a0dfd Mon Sep 17 00:00:00 2001 From: godofredoc Date: Fri, 5 Nov 2021 15:53:46 -0700 Subject: [PATCH 143/259] Add mirroring of master to main branch. (#4477) * Add mirroring of master to main branch. This is to prepare for the migration of master to main. Bug: https://github.com/flutter/flutter/issues/90476 * Update description. --- .github/workflows/mirror.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/mirror.yml diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 000000000000..47af473696c4 --- /dev/null +++ b/.github/workflows/mirror.yml @@ -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. + +# Mirror master to main branches in the plugins repository. +on: + push: + branches: + - 'master' + +jobs: + mirror_job: + permissions: + pull-requests: write + runs-on: ubuntu-latest + if: ${{ github.repository == 'flutter/plugins' }} + name: Mirror master branch to main branch + steps: + - name: Mirror action step + id: mirror + uses: google/mirror-branch-action@c6b07e441a7ffc5ae15860c1d0a8107a3a151db8 + with: + github-token: ${{ secrets.FLUTTERGITHUBBOT_TOKEN }} + source: 'master' + dest: 'main' From 638dd07f51fa64c953e07272608451c98c67972e Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Mon, 8 Nov 2021 18:39:58 -0800 Subject: [PATCH 144/259] Disable camera/quick_actions tests temporarily (#4487) --- script/configs/exclude_native_unit_android.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml index 5ec80eee73a0..1437e8b23c62 100644 --- a/script/configs/exclude_native_unit_android.yaml +++ b/script/configs/exclude_native_unit_android.yaml @@ -9,3 +9,7 @@ # No need for unit tests: - espresso + +# TODO(egarciad): Re-enable these tests https://github.com/flutter/flutter/issues/93286 +- camera/camera +- quick_actions/quick_actions From 1b8b8dadc6db4d2ff769a7815c0c8429031cd616 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 9 Nov 2021 11:36:28 -0800 Subject: [PATCH 145/259] [google_maps_flutter] Disable XCUITest (#4490) Temporarily disables the only XCUITest for Maps until the cause of https://github.com/flutter/flutter/issues/93325 is found and fixed, in order to re-open the tree. --- .../example/ios/RunnerUITests/GoogleMapsUITests.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m index f56a2d17e3fe..9a8ded716ec6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m @@ -42,7 +42,8 @@ - (void)setUp { }]; } -- (void)testUserInterface { +// Temporarily disabled due to https://github.com/flutter/flutter/issues/93325 +- (void)skip_testUserInterface { XCUIApplication* app = self.app; XCUIElement* userInteface = app.staticTexts[@"User interface"]; if (![userInteface waitForExistenceWithTimeout:30.0]) { From 68d222342e39c9fe3e7d87c5db63a02a95dda826 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 9 Nov 2021 12:43:07 -0800 Subject: [PATCH 146/259] [video_player] Don't restart when scrubbing to end (#4488) --- .../video_player/video_player/CHANGELOG.md | 5 ++ .../video_player/lib/video_player.dart | 3 +- .../video_player/video_player/pubspec.yaml | 2 +- .../video_player/test/video_player_test.dart | 54 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 9eb9d24679d0..b0db4dcc9909 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.7 + +* Fixes a regression where dragging a [VideoProgressIndicator] while playing + would restart playback from the start of the video. + ## 2.2.6 * Initialize player when size and duration become available on iOS diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index fe3437593a81..ff727f1432c8 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -784,7 +784,8 @@ class _VideoScrubberState extends State<_VideoScrubber> { seekToRelativePosition(details.globalPosition); }, onHorizontalDragEnd: (DragEndDetails details) { - if (_controllerWasPlaying) { + if (_controllerWasPlaying && + controller.value.position != controller.value.duration) { controller.play(); } }, diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c002ed859f2c..f0d5951b1403 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.6 +version: 2.2.7 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 a929c6827fd0..08fd9dc604f1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -478,6 +478,60 @@ void main() { }); }); + group('scrubbing', () { + testWidgets('restarts on release if already playing', + (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + final progressWidget = + VideoProgressIndicator(controller, allowScrubbing: true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + )); + + await controller.play(); + expect(controller.value.isPlaying, isTrue); + + final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); + await tester.dragFrom(progressRect.center, Offset(1.0, 0.0)); + await tester.pumpAndSettle(); + + expect(controller.value.position, lessThan(controller.value.duration)); + expect(controller.value.isPlaying, isTrue); + + await controller.pause(); + }); + + testWidgets('does not restart when dragging to end', + (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + final progressWidget = + VideoProgressIndicator(controller, allowScrubbing: true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + )); + + await controller.play(); + expect(controller.value.isPlaying, isTrue); + + final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); + await tester.dragFrom(progressRect.center, progressRect.centerRight); + await tester.pumpAndSettle(); + + expect(controller.value.position, controller.value.duration); + expect(controller.value.isPlaying, isFalse); + }); + }); + group('caption', () { test('works when seeking', () async { final VideoPlayerController controller = VideoPlayerController.network( From 1a2fbe878d86ee74fc46a422619442d4c5a74945 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 9 Nov 2021 16:53:03 -0800 Subject: [PATCH 147/259] Adjust pull request template (#4478) --- .github/PULL_REQUEST_TEMPLATE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a3a279ab2151..47d4d48adf7d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,14 +8,14 @@ - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. -- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. (Note that unlike the flutter/flutter repo, the flutter/plugins repo does use `dart format`.) +- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. (Unlike the flutter/flutter repo, the flutter/plugins repo does use `dart format`.) - [ ] I signed the [CLA]. - [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. `[shared_preferences]` - [ ] I listed at least one issue that this PR fixes in the description above. -- [ ] I [updated pubspec.yaml](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates) with an appropriate new version according to the [pub versioning philosophy]. -- [ ] I updated CHANGELOG.md to add a description of the change. +- [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. +- [ ] I updated `CHANGELOG.md` to add a description of the change. - [ ] I updated/added relevant documentation (doc comments with `///`). -- [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt. +- [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. @@ -29,4 +29,6 @@ If you need help, consider asking for advice on the #hackers-new channel on [Dis [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning +[exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [the auto-formatter]: https://github.com/flutter/plugins/blob/master/script/tool/README.md#format-code +[test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests From d9d87e9f88095add06b5f1945f2235927b5f4d81 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 10 Nov 2021 11:09:21 -0500 Subject: [PATCH 148/259] [ci] Increase Android and Web sharding (#4493) Currently most of our Cirrus task runs are about 20 minutes or less, but there are a few consistent outliers: - One Android shard takes ~40 minutes, and another takes 25-30. - The Web platform tests take ~40 minutes. This increases Android sharding from 4 to 5, and web sharding from 1 (no sharding) to 2 to improve overall test run times. --- .cirrus.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index da354c5a9b22..e5593397e270 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -174,10 +174,11 @@ task: - name: android-platform_tests env: matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 5" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 5" + PLUGIN_SHARDING: "--shardIndex 2 --shardCount 5" + PLUGIN_SHARDING: "--shardIndex 3 --shardCount 5" + PLUGIN_SHARDING: "--shardIndex 4 --shardCount 5" matrix: CHANNEL: "master" CHANNEL: "stable" @@ -228,6 +229,9 @@ task: ### Web tasks ### - name: web-platform_tests env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" matrix: CHANNEL: "master" CHANNEL: "stable" From 020c855ecbc17c94edf69b26b9e6f07e6b54c442 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 10 Nov 2021 14:46:21 -0500 Subject: [PATCH 149/259] Remove "pluginClass: none" from pubspecs (#4475) This workaround has not been needed since Flutter 1.20, and all plugins now require at least Flutter 2.0, so it can be safely removed. Part of https://github.com/flutter/flutter/issues/57497 --- packages/path_provider/path_provider_linux/CHANGELOG.md | 4 ++++ packages/path_provider/path_provider_linux/pubspec.yaml | 3 +-- packages/path_provider/path_provider_windows/CHANGELOG.md | 4 ++++ packages/path_provider/path_provider_windows/pubspec.yaml | 3 +-- .../shared_preferences/shared_preferences_linux/CHANGELOG.md | 3 ++- .../shared_preferences/shared_preferences_linux/pubspec.yaml | 3 +-- .../shared_preferences_windows/CHANGELOG.md | 3 ++- .../shared_preferences_windows/pubspec.yaml | 3 +-- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 6f18d0d6ae58..a3febb0d108b 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Removed obsolete `pluginClass: none` from pubpsec. + ## 2.1.0 * Now `getTemporaryPath` returns the value of the `TMPDIR` environment variable primarily. If `TMPDIR` is not set, `/tmp` is returned. diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index f5b7a88ca232..bc1788729832 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -14,7 +14,6 @@ flutter: platforms: linux: dartPluginClass: PathProviderLinux - pluginClass: none dependencies: flutter: diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index 953bb894de09..d7eaeeef133d 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 + +* Removed obsolete `pluginClass: none` from pubpsec. + ## 2.0.3 * Updated installation instructions in README. diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 0353574b6235..78d403a8576c 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -14,7 +14,6 @@ flutter: platforms: windows: dartPluginClass: PathProviderWindows - pluginClass: none dependencies: ffi: ^1.0.0 diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 6fd14eebc7f0..e0178ed23879 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.3 +* Removed obsolete `pluginClass: none` from pubpsec. * Fixes newly enabled analyzer options. ## 2.0.2 diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index b6d1765c08f3..9eb4bcdea4cf 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -14,7 +14,6 @@ flutter: platforms: linux: dartPluginClass: SharedPreferencesLinux - pluginClass: none dependencies: file: ^6.0.0 diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 68358a3c354e..544322f2ca0f 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.0.3 +* Removed obsolete `pluginClass: none` from pubpsec. * Fixes newly enabled analyzer options. ## 2.0.2 diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 76b3c74628b4..cae748dee49c 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: '>=2.12.0 <3.0.0' @@ -14,7 +14,6 @@ flutter: platforms: windows: dartPluginClass: SharedPreferencesWindows - pluginClass: none dependencies: file: ^6.0.0 From bc958539596facc39a09ad8452866d2241f21f4e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 10 Nov 2021 14:46:35 -0500 Subject: [PATCH 150/259] [flutter_plugin_tools] Add 'main' support (#4474) Treat `main` the same as `master` for branch-based switching, in preparation for switching the branch names in Flutter repositories. Also updates all of the tests that used `master` as the explicit base to use `main` instead; what the tests use is arbitrary, so they can be switched now even though the repo itself hasn't switched. Part of https://github.com/flutter/flutter/issues/90476 --- script/tool/CHANGELOG.md | 4 +- .../tool/lib/src/common/plugin_command.dart | 5 +- script/tool/lib/src/test_command.dart | 2 +- .../tool/lib/src/version_check_command.dart | 6 +- script/tool/pubspec.yaml | 2 +- script/tool/test/analyze_command_test.dart | 2 +- .../tool/test/common/plugin_command_test.dart | 117 +++++++--------- .../tool/test/pubspec_check_command_test.dart | 2 +- .../tool/test/version_check_command_test.dart | 130 ++++++++---------- 9 files changed, 122 insertions(+), 148 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 89bc7759b962..aa931391a93a 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.7.2 - Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't @@ -15,6 +15,8 @@ length. - Fix `license-check` when run on Windows with line ending conversion enabled. - Fixed `pubspec-check` on Windows. +- Add support for `main` as a primary branch. `master` continues to work for + compatibility. ## 0.7.1 diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 5d5cbd9abf6c..6708f9101130 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -81,7 +81,8 @@ abstract class PluginCommand extends Command { argParser.addFlag(_packagesForBranchArg, help: 'This runs on all packages (equivalent to no package selection flag)\n' - 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'on main (or master), and behaves like --run-on-changed-packages on ' + 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); @@ -301,7 +302,7 @@ abstract class PluginCommand extends Command { 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { - runOnChangedPackages = branch != 'master'; + runOnChangedPackages = branch != 'master' && branch != 'main'; // Log the mode for auditing what was intended to run. print('--$_packagesForBranchArg: running on ' '${runOnChangedPackages ? 'changed' : 'all'} packages'); diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 5a0b43d3b223..ee3540d90d3c 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -24,7 +24,7 @@ class TestCommand extends PackageLoopingCommand { defaultsTo: '', help: 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' - 'See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md ' + 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' 'for details.', ); } diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 90ba0668002b..dc664e53d7fd 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -262,7 +262,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // This method isn't called unless `version` is non-null. final Version currentVersion = pubspec.version!; Version? previousVersion; + String previousVersionSource; if (getBoolArg(_againstPubFlag)) { + previousVersionSource = 'pub'; previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { return _CurrentVersionState.unknown; @@ -273,6 +275,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } else { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + previousVersionSource = await gitVersionFinder.getBaseSha(); previousVersion = await _getPreviousVersionFromGit(package, gitVersionFinder: gitVersionFinder) ?? Version.none; @@ -310,9 +313,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (allowedNextVersions.containsKey(currentVersion)) { print('$indentation$previousVersion -> $currentVersion'); } else { - final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' + '${indentation}HEAD: $currentVersion, $previousVersionSource: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); return _CurrentVersionState.invalidChange; } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 689618f06123..8cde4dd46d80 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/master/script/tool -version: 0.7.1 +version: 0.7.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 502fa9a0634c..878facd83c06 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -253,7 +253,7 @@ void main() { }); // Ensure that the command used to analyze flutter/plugins in the Dart repo: - // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh // continues to work. // // DO NOT remove or modify this test without a coordination plan in place to diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 13724e26e5f8..6d586e416b7d 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -246,11 +246,8 @@ void main() { test('all plugins should be tested if there are no changes.', () async { final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -264,11 +261,8 @@ void main() { ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -283,11 +277,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -302,11 +293,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -322,11 +310,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -342,11 +327,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -362,11 +344,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -382,11 +361,8 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -398,17 +374,14 @@ packages/plugin1/CHANGELOG ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final List output = await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( output, containsAllInOrder([ contains( - 'Running for all packages that have changed relative to "master"'), + 'Running for all packages that have changed relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); @@ -424,11 +397,8 @@ packages/plugin1/ios/plugin1.m ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); @@ -444,11 +414,8 @@ packages/plugin2/ios/plugin2.m final Directory plugin1 = createFakePlugin('plugin1', packagesDir); final Directory plugin2 = createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -468,11 +435,8 @@ packages/plugin1/plugin1_web/plugin1_web.dart createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); @@ -491,11 +455,8 @@ packages/plugin1/plugin1/plugin1.dart packagesDir.childDirectory('plugin1')); final Directory plugin3 = createFakePlugin( 'plugin1_web', packagesDir.childDirectory('plugin1')); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( command.plugins, @@ -518,7 +479,7 @@ packages/plugin3/plugin3.dart await runCapturingPrint(runner, [ 'sample', '--exclude=plugin2,plugin3', - '--base-sha=master', + '--base-sha=main', '--run-on-changed-packages' ]); @@ -549,6 +510,28 @@ packages/plugin3/plugin3.dart ])); }); + test('tests all packages on main', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'main'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + test('tests all packages on master', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index ba943903b6c5..9ad1eaa620c4 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -38,7 +38,7 @@ String _headerSection( 'flutter', if (isPlugin) 'plugins' else 'packages', 'tree', - 'master', + 'main', 'packages', repositoryPath, ]; diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 39132212d664..30b8855b5a3f 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -22,15 +22,15 @@ import 'mocks.dart'; import 'util.dart'; void testAllowedVersion( - String masterVersion, + String mainVersion, String headVersion, { bool allowed = true, NextVersionType? nextVersionType, }) { - final Version master = Version.parse(masterVersion); + final Version main = Version.parse(mainVersion); final Version head = Version.parse(headVersion); final Map allowedVersions = - getAllowedNextVersions(master, newVersion: head); + getAllowedNextVersions(main, newVersion: head); if (allowed) { expect(allowedVersions, contains(head)); if (nextVersionType != null) { @@ -109,10 +109,10 @@ void main() { test('allows valid version', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, @@ -125,17 +125,17 @@ void main() { expect( gitDirCommands, containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'main:packages/plugin/pubspec.yaml']), ])); }); test('denies invalid version', () async { createFakePlugin('plugin', packagesDir, version: '0.2.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'main:packages/plugin/pubspec.yaml': 'version: 0.0.1', }; final Future> result = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( result, @@ -145,7 +145,7 @@ void main() { expect( gitDirCommands, containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'main:packages/plugin/pubspec.yaml']), ])); }); @@ -229,11 +229,11 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '1.1.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -247,7 +247,7 @@ void main() { containsAll([ equals([ 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' + 'main:packages/plugin_platform_interface/pubspec.yaml' ]), ])); }); @@ -257,11 +257,11 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( output, throwsA(isA()), @@ -272,7 +272,7 @@ void main() { containsAll([ equals([ 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' + 'main:packages/plugin_platform_interface/pubspec.yaml' ]), ])); }); @@ -282,7 +282,7 @@ void main() { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final File changeDescriptionFile = @@ -297,7 +297,7 @@ This is necessary because of X, Y, and Z ## Another section'''); final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--change-description-file=${changeDescriptionFile.path}' ]); @@ -317,14 +317,14 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--change-description-file=a_missing_file.txt' ], errorHandler: (Error e) { commandError = e; @@ -344,12 +344,12 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': + 'main:packages/plugin_platform_interface/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', '--ignore-platform-interface-breaks' ]); @@ -375,7 +375,7 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -393,11 +393,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -422,7 +420,7 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -445,11 +443,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -477,11 +473,11 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); await expectLater( output, containsAllInOrder([ @@ -506,11 +502,9 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -541,15 +535,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -578,15 +570,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -615,13 +605,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', ], errorHandler: (Error e) { commandError = e; }); @@ -648,13 +638,13 @@ This is necessary because of X, Y, and Z '''; createFakeCHANGELOG(pluginDirectory, changelog); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; Error? commandError; final List output = await runCapturingPrint(runner, [ 'version-check', - '--base-sha=master', + '--base-sha=main', ], errorHandler: (Error e) { commandError = e; }); @@ -680,10 +670,10 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List output = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( output, @@ -704,15 +694,13 @@ This is necessary because of X, Y, and Z createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -736,14 +724,12 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -767,10 +753,10 @@ ${indentation}HTTP response: null createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'main:packages/plugin/pubspec.yaml': 'version: 1.0.0', }; final List result = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( result, From 0cfbe1779ecbdb781b3efc830ffd1701cda65cf8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 10 Nov 2021 14:48:23 -0500 Subject: [PATCH 151/259] [path_provider] Federate mobile implementations (#4465) Fully federates the plugin by moving the existing mobile implementations to their own packages, per planned repo structure. Temporarily marks `path_provider` as unpublishable to allow the implementations to be moved, rather than copied and deleted, in order to better preserve git history. A follow-up PR will restore it to publishable form. Part of https://github.com/flutter/flutter/issues/68498 --- .../path_provider/path_provider/CHANGELOG.md | 4 + .../path_provider/android/settings.gradle | 1 - .../path_provider/path_provider/pubspec.yaml | 15 +- .../path_provider_android/AUTHORS | 66 ++ .../path_provider_android/CHANGELOG.md | 3 + .../path_provider_android/LICENSE | 25 + .../path_provider_android/README.md | 11 + .../android/build.gradle | 0 .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 0 .../pathprovider/PathProviderPlugin.java | 0 .../pathprovider/StorageDirectoryMapper.java | 0 .../StorageDirectoryMapperTest.java | 0 .../path_provider_android/example/README.md | 8 + .../example/android/app/build.gradle | 64 ++ .../gradle/wrapper/gradle-wrapper.properties | 5 + .../flutter/plugins/DartIntegrationTest.java | 14 + .../pathprovider/MainActivityTest.java | 19 + .../android/app/src/main/AndroidManifest.xml | 20 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../example/android/build.gradle | 29 + .../example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle | 15 + .../integration_test/path_provider_test.dart | 96 +++ .../example/lib/main.dart | 189 ++++++ .../example/pubspec.yaml | 29 + .../example/test_driver/integration_test.dart | 7 + .../path_provider_android/pubspec.yaml | 33 + .../path_provider/path_provider_ios/AUTHORS | 66 ++ .../path_provider_ios/CHANGELOG.md | 3 + .../path_provider/path_provider_ios/LICENSE | 25 + .../path_provider/path_provider_ios/README.md | 11 + .../path_provider_ios/example/README.md | 8 + .../integration_test/path_provider_test.dart | 69 ++ .../ios/Flutter/AppFrameworkInfo.plist | 30 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../path_provider_ios/example/ios/Podfile | 41 ++ .../ios/Runner.xcodeproj/project.pbxproj | 607 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 97 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/Runner/AppDelegate.h | 10 + .../example/ios/Runner/AppDelegate.m | 16 + .../AppIcon.appiconset/Contents.json | 116 ++++ .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../Runner/Base.lproj/LaunchScreen.storyboard | 27 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 49 ++ .../example/ios/Runner/main.m | 13 + .../example/ios/RunnerTests/Info.plist | 22 + .../ios/RunnerTests/PathProviderTests.m | 18 + .../path_provider_ios/example/lib/main.dart | 131 ++++ .../path_provider_ios/example/pubspec.yaml | 29 + .../example/test_driver/integration_test.dart | 7 + .../ios/Assets/.gitkeep | 0 .../ios/Classes/FLTPathProviderPlugin.h | 0 .../ios/Classes/FLTPathProviderPlugin.m | 0 .../ios/path_provider_ios.podspec} | 5 +- .../path_provider_ios/pubspec.yaml | 32 + 79 files changed, 2142 insertions(+), 8 deletions(-) delete mode 100644 packages/path_provider/path_provider/android/settings.gradle create mode 100644 packages/path_provider/path_provider_android/AUTHORS create mode 100644 packages/path_provider/path_provider_android/CHANGELOG.md create mode 100644 packages/path_provider/path_provider_android/LICENSE create mode 100644 packages/path_provider/path_provider_android/README.md rename packages/path_provider/{path_provider => path_provider_android}/android/build.gradle (100%) create mode 100644 packages/path_provider/path_provider_android/android/settings.gradle rename packages/path_provider/{path_provider => path_provider_android}/android/src/main/AndroidManifest.xml (100%) rename packages/path_provider/{path_provider => path_provider_android}/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java (100%) rename packages/path_provider/{path_provider => path_provider_android}/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java (100%) rename packages/path_provider/{path_provider => path_provider_android}/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java (100%) create mode 100644 packages/path_provider/path_provider_android/example/README.md create mode 100644 packages/path_provider/path_provider_android/example/android/app/build.gradle create mode 100644 packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/path_provider/path_provider_android/example/android/build.gradle create mode 100644 packages/path_provider/path_provider_android/example/android/gradle.properties create mode 100644 packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/path_provider/path_provider_android/example/android/settings.gradle create mode 100644 packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart create mode 100644 packages/path_provider/path_provider_android/example/lib/main.dart create mode 100644 packages/path_provider/path_provider_android/example/pubspec.yaml create mode 100644 packages/path_provider/path_provider_android/example/test_driver/integration_test.dart create mode 100644 packages/path_provider/path_provider_android/pubspec.yaml create mode 100644 packages/path_provider/path_provider_ios/AUTHORS create mode 100644 packages/path_provider/path_provider_ios/CHANGELOG.md create mode 100644 packages/path_provider/path_provider_ios/LICENSE create mode 100644 packages/path_provider/path_provider_ios/README.md create mode 100644 packages/path_provider/path_provider_ios/example/README.md create mode 100644 packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart create mode 100644 packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig create mode 100644 packages/path_provider/path_provider_ios/example/ios/Podfile create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist create mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/main.m create mode 100644 packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist create mode 100644 packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m create mode 100644 packages/path_provider/path_provider_ios/example/lib/main.dart create mode 100644 packages/path_provider/path_provider_ios/example/pubspec.yaml create mode 100644 packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart rename packages/path_provider/{path_provider => path_provider_ios}/ios/Assets/.gitkeep (100%) rename packages/path_provider/{path_provider => path_provider_ios}/ios/Classes/FLTPathProviderPlugin.h (100%) rename packages/path_provider/{path_provider => path_provider_ios}/ios/Classes/FLTPathProviderPlugin.m (100%) rename packages/path_provider/{path_provider/ios/path_provider.podspec => path_provider_ios/ios/path_provider_ios.podspec} (89%) create mode 100644 packages/path_provider/path_provider_ios/pubspec.yaml diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 568dd2fde499..28c233266042 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.7 + +* Moved Android and iOS implementations to federated packages. + ## 2.0.6 * Added support for Background Platform Channels on Android when it is diff --git a/packages/path_provider/path_provider/android/settings.gradle b/packages/path_provider/path_provider/android/settings.gradle deleted file mode 100644 index 71bc90768477..000000000000 --- a/packages/path_provider/path_provider/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'path_provider' diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 12a186902736..8757462a449c 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,7 +2,10 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.6 +version: 2.0.7 +# Temporarily disable publishing to allow moving Android and iOS +# implementations. +publish_to: none environment: sdk: ">=2.14.0 <3.0.0" @@ -12,10 +15,9 @@ flutter: plugin: platforms: android: - package: io.flutter.plugins.pathprovider - pluginClass: PathProviderPlugin + default_package: path_provider_android ios: - pluginClass: FLTPathProviderPlugin + default_package: path_provider_ios macos: default_package: path_provider_macos linux: @@ -26,6 +28,11 @@ flutter: dependencies: flutter: sdk: flutter + # Temporary path dependencies to allow moving Android and iOS implementations. + path_provider_android: + path: ../path_provider_android + path_provider_ios: + path: ../path_provider_ios path_provider_linux: ^2.0.0 path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 diff --git a/packages/path_provider/path_provider_android/AUTHORS b/packages/path_provider/path_provider_android/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md new file mode 100644 index 000000000000..dc24db654c90 --- /dev/null +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2.0.6 + +* Split from `path_provider` as a federated implementation. diff --git a/packages/path_provider/path_provider_android/LICENSE b/packages/path_provider/path_provider_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/README.md b/packages/path_provider/path_provider_android/README.md new file mode 100644 index 000000000000..7f2603d30e06 --- /dev/null +++ b/packages/path_provider/path_provider_android/README.md @@ -0,0 +1,11 @@ +# path\_provider\_android + +The Android implementation of [`path_provider`]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle similarity index 100% rename from packages/path_provider/path_provider/android/build.gradle rename to packages/path_provider/path_provider_android/android/build.gradle diff --git a/packages/path_provider/path_provider_android/android/settings.gradle b/packages/path_provider/path_provider_android/android/settings.gradle new file mode 100644 index 000000000000..359a57ff9540 --- /dev/null +++ b/packages/path_provider/path_provider_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'path_provider_android' diff --git a/packages/path_provider/path_provider/android/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/path_provider/path_provider/android/src/main/AndroidManifest.xml rename to packages/path_provider/path_provider_android/android/src/main/AndroidManifest.xml diff --git a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java similarity index 100% rename from packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java rename to packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java diff --git a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java similarity index 100% rename from packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java rename to packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java diff --git a/packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java b/packages/path_provider/path_provider_android/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java similarity index 100% rename from packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java rename to packages/path_provider/path_provider_android/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java diff --git a/packages/path_provider/path_provider_android/example/README.md b/packages/path_provider/path_provider_android/example/README.md new file mode 100644 index 000000000000..1f8ea7189ccd --- /dev/null +++ b/packages/path_provider/path_provider_android/example/README.md @@ -0,0 +1,8 @@ +# path_provider_example + +Demonstrates how to use the path_provider plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider_android/example/android/app/build.gradle b/packages/path_provider/path_provider_android/example/android/app/build.gradle new file mode 100644 index 000000000000..e7f1bfb111a2 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/app/build.gradle @@ -0,0 +1,64 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.pathproviderexample" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..9a4163a4f5ee --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java new file mode 100644 index 000000000000..d56458bd753c --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.pathprovider; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..df8cee7bc3be --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_android/example/android/build.gradle b/packages/path_provider/path_provider_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/example/android/gradle.properties b/packages/path_provider/path_provider_android/example/android/gradle.properties new file mode 100644 index 000000000000..38c8d4544ff1 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..caf54fa2801c --- /dev/null +++ b/packages/path_provider/path_provider_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-5.1.1-all.zip diff --git a/packages/path_provider/path_provider_android/example/android/settings.gradle b/packages/path_provider/path_provider_android/example/android/settings.gradle new file mode 100644 index 000000000000..6cb349eef1b6 --- /dev/null +++ b/packages/path_provider/path_provider_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 +} \ No newline at end of file diff --git a/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart new file mode 100644 index 000000000000..426b07abc7c1 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getLibraryPath(), + throwsA(isInstanceOf())); + }); + + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getExternalStoragePath(); + _verifySampleFile(result, 'externalStorage'); + }); + + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final List? directories = await provider.getExternalCachePaths(); + expect(directories, isNotNull); + for (final String result in directories!) { + _verifySampleFile(result, 'externalCache'); + } + }); + + final List _allDirs = [ + null, + StorageDirectory.music, + StorageDirectory.podcasts, + StorageDirectory.ringtones, + StorageDirectory.alarms, + StorageDirectory.notifications, + StorageDirectory.pictures, + StorageDirectory.movies, + ]; + + for (final StorageDirectory? type in _allDirs) { + test('getExternalStorageDirectories (type: $type)', () async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + + final List? directories = + await provider.getExternalStoragePaths(type: type); + expect(directories, isNotNull); + for (final String result in directories!) { + _verifySampleFile(result, '$type'); + } + }); + } +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String? directoryPath, String name) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_android/example/lib/main.dart b/packages/path_provider/path_provider_android/example/lib/main.dart new file mode 100644 index 000000000000..6e04f865bfcf --- /dev/null +++ b/packages/path_provider/path_provider_android/example/lib/main.dart @@ -0,0 +1,189 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Path Provider', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Path Provider'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final PathProviderPlatform provider = PathProviderPlatform.instance; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appDocumentsDirectory; + Future? _externalDocumentsDirectory; + Future?>? _externalStorageDirectories; + Future?>? _externalCacheDirectories; + + void _requestTempDirectory() { + setState(() { + _tempDirectory = provider.getTemporaryPath(); + }); + } + + Widget _buildDirectory( + BuildContext context, AsyncSnapshot snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + text = Text('path: ${snapshot.data}'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + Widget _buildDirectories( + BuildContext context, AsyncSnapshot?> snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + final String combined = snapshot.data!.join(', '); + text = Text('paths: $combined'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + void _requestAppDocumentsDirectory() { + setState(() { + _appDocumentsDirectory = provider.getApplicationDocumentsPath(); + }); + } + + void _requestAppSupportDirectory() { + setState(() { + _appSupportDirectory = provider.getApplicationSupportPath(); + }); + } + + void _requestExternalStorageDirectory() { + setState(() { + _externalDocumentsDirectory = provider.getExternalStoragePath(); + }); + } + + void _requestExternalStorageDirectories(StorageDirectory type) { + setState(() { + _externalStorageDirectories = + provider.getExternalStoragePaths(type: type); + }); + } + + void _requestExternalCacheDirectories() { + setState(() { + _externalCacheDirectories = provider.getExternalCachePaths(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Temporary Directory'), + onPressed: _requestTempDirectory, + ), + ), + FutureBuilder( + future: _tempDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Application Documents Directory'), + onPressed: _requestAppDocumentsDirectory, + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Application Support Directory'), + onPressed: _requestAppSupportDirectory, + ), + ), + FutureBuilder( + future: _appSupportDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get External Storage Directory'), + onPressed: _requestExternalStorageDirectory, + ), + ), + FutureBuilder( + future: _externalDocumentsDirectory, builder: _buildDirectory), + Column(children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get External Storage Directories'), + onPressed: () { + _requestExternalStorageDirectories( + StorageDirectory.music, + ); + }, + ), + ), + ]), + FutureBuilder?>( + future: _externalStorageDirectories, + builder: _buildDirectories), + Column(children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get External Cache Directories'), + onPressed: _requestExternalCacheDirectories, + ), + ), + ]), + FutureBuilder?>( + future: _externalCacheDirectories, builder: _buildDirectories), + ], + ), + ), + ); + } +} diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml new file mode 100644 index 000000000000..08f2c6d4b5b4 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + path_provider_android: + # When depending on this package from a real application you should use: + # path_provider: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_android/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml new file mode 100644 index 000000000000..3d625c08a9a6 --- /dev/null +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -0,0 +1,33 @@ +name: path_provider_android +description: Android implementation of the path_provider plugin. +repository: https://github.com/flutter/plugins/tree/master/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.6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: path_provider + platforms: + android: + package: io.flutter.plugins.pathprovider + pluginClass: PathProviderPlugin + +dependencies: + flutter: + sdk: flutter + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + plugin_platform_interface: ^2.0.0 + test: ^1.16.0 diff --git a/packages/path_provider/path_provider_ios/AUTHORS b/packages/path_provider/path_provider_ios/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md new file mode 100644 index 000000000000..dc24db654c90 --- /dev/null +++ b/packages/path_provider/path_provider_ios/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2.0.6 + +* Split from `path_provider` as a federated implementation. diff --git a/packages/path_provider/path_provider_ios/LICENSE b/packages/path_provider/path_provider_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/README.md b/packages/path_provider/path_provider_ios/README.md new file mode 100644 index 000000000000..b170d1762f41 --- /dev/null +++ b/packages/path_provider/path_provider_ios/README.md @@ -0,0 +1,11 @@ +# path\_provider\_ios + +The iOS implementation of [`path_provider`]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_ios/example/README.md b/packages/path_provider/path_provider_ios/example/README.md new file mode 100644 index 000000000000..1f8ea7189ccd --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/README.md @@ -0,0 +1,8 @@ +# path_provider_example + +Demonstrates how to use the path_provider plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart new file mode 100644 index 000000000000..5c6cf5a63579 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getLibraryPath(); + _verifySampleFile(result, 'library'); + }); + + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getExternalStoragePath(), + throwsA(isInstanceOf())); + }); + + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getExternalCachePaths(), + throwsA(isInstanceOf())); + }); +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String? directoryPath, String name) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Podfile b/packages/path_provider/path_provider_ios/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..fec246b9e08c --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,607 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */; }; + 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1DE26671E960040C8BC /* PathProviderTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F76AC1E126671E960040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F76AC1DC26671E960040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1DE26671E960040C8BC /* PathProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PathProviderTests.m; sourceTree = ""; }; + F76AC1E026671E960040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1D926671E960040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, + D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, + 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */, + 29F2567B3AE74A9113ED3394 /* 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 = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F76AC1DD26671E960040C8BC /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1DC26671E960040C8BC /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */, + 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */, + 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F76AC1DD26671E960040C8BC /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F76AC1DE26671E960040C8BC /* PathProviderTests.m */, + F76AC1E026671E960040C8BC /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 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"; + }; + F76AC1DB26671E960040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 31566AD39C1C7EF9EB261E6F /* [CP] Check Pods Manifest.lock */, + F76AC1D826671E960040C8BC /* Sources */, + F76AC1D926671E960040C8BC /* Frameworks */, + F76AC1DA26671E960040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1E226671E960040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC1DC26671E960040C8BC /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F76AC1DB26671E960040C8BC = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + F76AC1DB26671E960040C8BC /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1DA26671E960040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 31566AD39C1C7EF9EB261E6F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1D826671E960040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F76AC1E226671E960040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1E126671E960040C8BC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + 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; + 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.pathProviderExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F76AC1E326671E960040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F76AC1E426671E960040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; +/* 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; + }; + F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1E326671E960040C8BC /* Debug */, + F76AC1E426671E960040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..ec3713b95db5 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h b/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..b790a0a52635 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..342db6a5dcaf --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + path_provider_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/main.m b/packages/path_provider/path_provider_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m new file mode 100644 index 000000000000..0f746cc81b93 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import path_provider_ios; +@import XCTest; + +@interface PathProviderTests : XCTestCase +@end + +@implementation PathProviderTests + +- (void)testPlugin { + FLTPathProviderPlugin* plugin = [[FLTPathProviderPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/path_provider/path_provider_ios/example/lib/main.dart b/packages/path_provider/path_provider_ios/example/lib/main.dart new file mode 100644 index 000000000000..8c8d5410f923 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/lib/main.dart @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Path Provider', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Path Provider'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final PathProviderPlatform provider = PathProviderPlatform.instance; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appLibraryDirectory; + Future? _appDocumentsDirectory; + + void _requestTempDirectory() { + setState(() { + _tempDirectory = provider.getTemporaryPath(); + }); + } + + Widget _buildDirectory( + BuildContext context, AsyncSnapshot snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + text = Text('path: ${snapshot.data}'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + void _requestAppDocumentsDirectory() { + setState(() { + _appDocumentsDirectory = provider.getApplicationDocumentsPath(); + }); + } + + void _requestAppSupportDirectory() { + setState(() { + _appSupportDirectory = provider.getApplicationSupportPath(); + }); + } + + void _requestAppLibraryDirectory() { + setState(() { + _appLibraryDirectory = provider.getLibraryPath(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Temporary Directory'), + onPressed: _requestTempDirectory, + ), + ), + FutureBuilder( + future: _tempDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Application Documents Directory'), + onPressed: _requestAppDocumentsDirectory, + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Application Support Directory'), + onPressed: _requestAppSupportDirectory, + ), + ), + FutureBuilder( + future: _appSupportDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get Application Library Directory'), + onPressed: _requestAppLibraryDirectory, + ), + ), + FutureBuilder( + future: _appLibraryDirectory, builder: _buildDirectory), + ], + ), + ), + ); + } +} diff --git a/packages/path_provider/path_provider_ios/example/pubspec.yaml b/packages/path_provider/path_provider_ios/example/pubspec.yaml new file mode 100644 index 000000000000..ebe846e3c502 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + path_provider_ios: + # When depending on this package from a real application you should use: + # path_provider_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: ../ + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider/ios/Assets/.gitkeep b/packages/path_provider/path_provider_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/path_provider/path_provider/ios/Assets/.gitkeep rename to packages/path_provider/path_provider_ios/ios/Assets/.gitkeep diff --git a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h similarity index 100% rename from packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h rename to packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h diff --git a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m similarity index 100% rename from packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m rename to packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m diff --git a/packages/path_provider/path_provider/ios/path_provider.podspec b/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec similarity index 89% rename from packages/path_provider/path_provider/ios/path_provider.podspec rename to packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec index 86f27c6c8fa5..5d79a49e045b 100644 --- a/packages/path_provider/path_provider/ios/path_provider.podspec +++ b/packages/path_provider/path_provider_ios/ios/path_provider_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 = 'path_provider' + s.name = 'path_provider_ios' s.version = '0.0.1' s.summary = 'Flutter Path Provider' s.description = <<-DESC @@ -12,7 +12,7 @@ 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/master/packages/path_provider/path_provider' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_ios' } s.documentation_url = 'https://pub.dev/packages/path_provider' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' @@ -20,4 +20,3 @@ Downloaded by pub (not CocoaPods). s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end - diff --git a/packages/path_provider/path_provider_ios/pubspec.yaml b/packages/path_provider/path_provider_ios/pubspec.yaml new file mode 100644 index 000000000000..d6d2bf29d9b2 --- /dev/null +++ b/packages/path_provider/path_provider_ios/pubspec.yaml @@ -0,0 +1,32 @@ +name: path_provider_ios +description: iOS implementation of the path_provider plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.0.6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: path_provider + platforms: + ios: + pluginClass: FLTPathProviderPlugin + +dependencies: + flutter: + sdk: flutter + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + plugin_platform_interface: ^2.0.0 + test: ^1.16.0 From 0f90d24f81ae21d0063a888cd59c9ad452acbed5 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 10 Nov 2021 22:08:04 +0100 Subject: [PATCH 152/259] [webview_flutter] Implement loadRequest in iOS package. (#4480) --- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 1 - .../example/ios/RunnerTests/FLTWebViewTests.m | 193 ++++++++++++++++++ .../example/lib/main.dart | 20 ++ .../example/lib/web_view.dart | 5 + .../ios/Classes/FlutterWebView.h | 1 + .../ios/Classes/FlutterWebView.m | 91 ++++++--- .../ios/Classes/FlutterWebView.modulemap | 10 + .../ios/Classes/FlutterWebView_Test.h | 21 ++ .../ios/Classes/webview-umbrella.h | 11 + .../ios/webview_flutter_wkwebview.podspec | 3 +- .../webview_flutter_wkwebview/pubspec.yaml | 4 +- 12 files changed, 334 insertions(+), 30 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 4db6dbfd2864..c1b19fd22d3e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Implemented new `loadRequest` method from platform interface. + ## 2.2.0 * Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. 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 ba0deb4781d4..2be87fbf81be 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 @@ -193,7 +193,6 @@ C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index 9d127c2c4aaa..61e43c104b57 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -5,6 +5,7 @@ @import Flutter; @import XCTest; @import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; // OCMock library doesn't generate a valid modulemap. #import @@ -301,4 +302,196 @@ - (void)testRunJavascriptReturningResultReturnsErrorResultForWKError { [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (void)testBuildNSURLRequestReturnsNilForNonDictionaryValue { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : @"Non Dictionary Value"}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestReturnsNilForMissingURI { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : @{}}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestReturnsNilForInvalidURI { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{@"uri" : @"invalid uri"}; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNil(request); +} + +- (void)testBuildNSURLRequestBuildsNSMutableURLRequestWithOptionalParameters { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{ + @"uri" : @"https://flutter.dev", + @"method" : @"POST", + @"headers" : @{@"Foo" : @"Bar"}, + @"body" : [FlutterStandardTypedData + typedDataWithBytes:[@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]], + }; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.URL.absoluteString, @"https://flutter.dev"); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + XCTAssertEqualObjects(request.allHTTPHeaderFields, @{@"Foo" : @"Bar"}); + XCTAssertEqualObjects(request.HTTPBody, [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]); +} + +- (void)testBuildNSURLRequestBuildsNSMutableURLRequestWithoutOptionalParameters { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + + // Run + NSDictionary *requestData = @{ + @"uri" : @"https://flutter.dev", + }; + NSURLRequest *request = [controller buildNSURLRequest:@{@"request" : requestData}]; + + // Verify + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.URL.absoluteString, @"https://flutter.dev"); + XCTAssertEqualObjects(request.HTTPMethod, @"GET"); + XCTAssertNil(request.allHTTPHeaderFields); + XCTAssertNil(request.HTTPBody); +} + +- (void)testOnLoadUrlReturnsErrorResultForInvalidRequest { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result when request cannot be built"]; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"loadUrl" + arguments:@{}]; + [controller onLoadUrl:methodCall + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadUrlLoadsRequestWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return nil"]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockView; + + // Run + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"loadUrl" + arguments:@{@"url" : @"https://flutter.dev/"}]; + [controller onLoadUrl:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + OCMVerify([mockView loadRequest:[OCMArg any]]); + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadRequestReturnsErroResultForInvalidRequest { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result when request cannot be built"]; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"loadRequest" + arguments:@{}]; + [controller onLoadRequest:methodCall + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testOnLoadRequestLoadsRequestWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return nil"]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockView; + + // Run + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"loadRequest" + arguments:@{@"request" : @{@"uri" : @"https://flutter.dev/"}}]; + [controller onLoadRequest:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + OCMVerify([mockView loadRequest:[OCMArg any]]); + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + @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 21240f63ec1a..ea95cddf9fc9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; +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'; @@ -120,6 +121,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + doPostRequest, } class _SampleMenu extends StatelessWidget { @@ -157,6 +159,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -189,6 +194,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), ], ); }, @@ -259,6 +268,17 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + void _onDoPostRequest( + WebViewController controller, BuildContext context) async { + WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index 403db1f08ac6..b2555cd831c6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -322,6 +322,11 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index db52124d6a7c..6d8e463c7b13 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -27,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIView*)view; - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; + @end @interface FLTWebViewFactory : NSObject 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 5e12f8acb2ea..b8355ad18183 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -5,6 +5,7 @@ #import "FlutterWebView.h" #import "FLTWKNavigationDelegate.h" #import "FLTWKProgressionDelegate.h" +#import "FlutterWebView_Test.h" #import "JavaScriptChannelHandler.h" @implementation FLTWebViewFactory { @@ -116,7 +117,11 @@ - (instancetype)initWithFrame:(CGRect)frame NSString* initialUrl = args[@"initialUrl"]; if ([initialUrl isKindOfClass:[NSString class]]) { - [self loadUrl:initialUrl]; + NSURL* url = [NSURL URLWithString:initialUrl]; + if (url) { + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + [_webView loadRequest:request]; + } } } return self; @@ -137,6 +142,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onUpdateSettings:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { [self onLoadUrl:call result:result]; + } else if ([[call method] isEqualToString:@"loadRequest"]) { + [self onLoadRequest:call result:result]; } else if ([[call method] isEqualToString:@"canGoBack"]) { [self onCanGoBack:call result:result]; } else if ([[call method] isEqualToString:@"canGoForward"]) { @@ -186,12 +193,34 @@ - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { } - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - if (![self loadRequest:[call arguments]]) { + NSMutableDictionary* requestData = [[NSMutableDictionary alloc] init]; + if (call.arguments[@"url"]) { + requestData[@"uri"] = call.arguments[@"url"]; + } + if (call.arguments[@"headers"]) { + requestData[@"headers"] = call.arguments[@"headers"]; + } + NSURLRequest* request = [self buildNSURLRequest:@{@"request" : requestData}]; + if (!request) { result([FlutterError errorWithCode:@"loadUrl_failed" message:@"Failed parsing the URL" details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); } else { + [_webView loadRequest:request]; + result(nil); + } +} + +- (void)onLoadRequest:(FlutterMethodCall*)call result:(FlutterResult)result { + NSURLRequest* request = [self buildNSURLRequest:[call arguments]]; + if (!request) { + result([FlutterError + errorWithCode:@"loadRequest_failed" + message:@"Failed parsing the URL" + details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); + } else { + [_webView loadRequest:request]; result(nil); } } @@ -459,37 +488,47 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy } } -- (bool)loadRequest:(NSDictionary*)request { - if (!request) { - return false; +/** + * Parses the method call arguments and converts them to an NSURLRequest object. + * + * @param arguments the method call arguments. + * + * @return NSURLRequest object. + */ +- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments { + id requestParameters = arguments[@"request"]; + if (![requestParameters isKindOfClass:[NSDictionary class]]) { + return nil; } - NSString* url = request[@"url"]; - if ([url isKindOfClass:[NSString class]]) { - id headers = request[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - return [self loadUrl:url withHeaders:headers]; - } else { - return [self loadUrl:url]; - } + NSString* urlString = requestParameters[@"uri"]; + if (!urlString) { + return nil; } - return false; -} + NSURL* url = [NSURL URLWithString:urlString]; + if (!url) { + return nil; + } -- (bool)loadUrl:(NSString*)url { - return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; -} + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; + + NSString* httpMethod = requestParameters[@"method"]; + if (httpMethod) { + [request setHTTPMethod:httpMethod]; + } -- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; + id httpBody = requestParameters[@"body"]; + if ([httpBody isKindOfClass:[FlutterStandardTypedData class]]) { + [request setHTTPBody:[httpBody data]]; } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; - [request setAllHTTPHeaderFields:headers]; - [_webView loadRequest:request]; - return true; + + id headers = requestParameters[@"headers"]; + if ([headers isKindOfClass:[NSDictionary class]]) { + [request setAllHTTPHeaderFields:headers]; + } + + return request; } - (void)registerJavaScriptChannels:(NSSet*)channelNames diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap new file mode 100644 index 000000000000..fa5143060381 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap @@ -0,0 +1,10 @@ +framework module webview_flutter_wkwebview { + umbrella header "webview-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FlutterWebView_Test.h" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h new file mode 100644 index 000000000000..13973f8c5c6e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView_Test.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import webview_flutter_wkwebview.Test;" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWebViewController () + +- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments; + +- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result; + +- (void)onLoadRequest:(FlutterMethodCall*)call result:(FlutterResult)result; + +@end + +NS_ASSUME_NONNULL_END 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 new file mode 100644 index 000000000000..c37282e886bc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-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 +#import +#import +#import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec index 2dfb336df35a..89c06237b203 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.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/master/packages/webview_flutter/webview_flutter_wkwebview' } s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/FlutterWebView.modulemap' s.dependency 'Flutter' s.platform = :ios, '9.0' diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 5176adb9749c..ff2b69fa8d20 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.2.0 + webview_flutter_platform_interface: ^1.3.0 dev_dependencies: flutter_driver: From 112fe4eb497a1dfdbd23dc10ac8e53898967cd4b Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:48:07 -0800 Subject: [PATCH 153/259] [package_info] Bump SDK version of example app (#4494) --- packages/package_info/CHANGELOG.md | 3 ++- packages/package_info/example/android/app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 0fe91175cf6b..7abe1c2694e1 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove references to the Android v1 embedding. * Updated Android lint settings. +* Update Android compileSdkVersion to 30. ## 2.0.2 @@ -30,7 +31,7 @@ ## 0.4.3+1 -* Update android compileSdkVersion to 29. +* Update Android compileSdkVersion to 29. ## 0.4.3 diff --git a/packages/package_info/example/android/app/build.gradle b/packages/package_info/example/android/app/build.gradle index 5099f3213fd8..0c55bdb1ef31 100644 --- a/packages/package_info/example/android/app/build.gradle +++ b/packages/package_info/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 lintOptions { disable 'InvalidPackage' From 76799fa023b61b34fec389739313ca27e8aad174 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 10 Nov 2021 15:41:39 -0800 Subject: [PATCH 154/259] Fix bugs in Dart Implementation of the Pigeon API (#4461) --- .../lib/src/android_webview.dart | 166 ++-- .../lib/src/android_webview.pigeon.dart | 309 ++++---- .../lib/src/android_webview_api_impls.dart | 155 ++-- .../pigeons/android_webview.dart | 14 +- .../webview_flutter_android/pubspec.yaml | 4 +- .../test/android_webview.pigeon.dart | 429 +++++------ .../test/android_webview_test.dart | 723 ++++++++++++------ .../test/android_webview_test.mocks.dart | 505 ++++++++++++ 8 files changed, 1527 insertions(+), 778 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart 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 1ddd6f3d9f0f..f6faa8e74331 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 @@ -29,6 +29,8 @@ const String _nullStringIdentifier = ''; /// To learn more about WebView and alternatives for serving web content, read /// the documentation on /// [Web-based content](https://developer.android.com/guide/webapps). +/// +/// When a [WebView] is no longer needed [release] must be called. class WebView { /// Constructs a new WebView. WebView({this.useHybridComposition = false}) { @@ -40,9 +42,6 @@ class WebView { static WebViewHostApiImpl api = WebViewHostApiImpl(); WebViewClient? _currentWebViewClient; - DownloadListener? _currentDownloadListener; - WebChromeClient? _currentWebChromeClient; - Set _javaScriptChannels = {}; /// Whether the [WebView] will be rendered with an [AndroidViewSurface]. /// @@ -187,18 +186,8 @@ class WebView { /// /// This will replace the current handler. Future setWebViewClient(WebViewClient webViewClient) { - final WebViewClient? currentWebViewClient = _currentWebViewClient; - - if (webViewClient == currentWebViewClient) { - return Future.value(); - } - - if (currentWebViewClient != null) { - WebViewClient.api.disposeFromInstance(currentWebViewClient); - } - - WebViewClient.api.createFromInstance(webViewClient); _currentWebViewClient = webViewClient; + WebViewClient.api.createFromInstance(webViewClient); return api.setWebViewClientFromInstance(this, webViewClient); } @@ -227,7 +216,6 @@ class WebView { /// content is ever loaded into the WebView even inside an iframe. Future addJavaScriptChannel(JavaScriptChannel javaScriptChannel) { JavaScriptChannel.api.createFromInstance(javaScriptChannel); - _javaScriptChannels.add(javaScriptChannel); return api.addJavaScriptChannelFromInstance(this, javaScriptChannel); } @@ -236,27 +224,15 @@ class WebView { /// Note that the removal will not be reflected in JavaScript until the page /// is next (re)loaded. See [addJavaScriptChannel]. Future removeJavaScriptChannel(JavaScriptChannel javaScriptChannel) { - _javaScriptChannels.remove(javaScriptChannel); - api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); - return JavaScriptChannel.api.disposeFromInstance(javaScriptChannel); + JavaScriptChannel.api.createFromInstance(javaScriptChannel); + return api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); } /// 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) { - final DownloadListener? currentDownloadListener = _currentDownloadListener; - - if (listener == currentDownloadListener) { - return Future.value(); - } - - if (currentDownloadListener != null) { - DownloadListener.api.disposeFromInstance(currentDownloadListener); - } - DownloadListener.api.createFromInstance(listener); - _currentDownloadListener = listener; return api.setDownloadListenerFromInstance(this, listener); } @@ -266,26 +242,26 @@ class WebView { /// JavaScript dialogs, favicons, titles, and the progress. This will replace /// the current handler. Future setWebChromeClient(WebChromeClient client) { - final WebChromeClient? currentWebChromeClient = _currentWebChromeClient; - - if (client == currentWebChromeClient) { - return Future.value(); - } - - if (currentWebChromeClient != null) { - WebChromeClient.api.disposeFromInstance(currentWebChromeClient); - } - - final WebViewClient? currentWebViewClient = _currentWebViewClient; + // 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 + // a secure url. assert( - currentWebViewClient != null, + _currentWebViewClient != null, "Can't set a WebChromeClient without setting a WebViewClient first.", ); - - WebChromeClient.api.createFromInstance(client, currentWebViewClient!); - _currentWebChromeClient = client; + WebChromeClient.api.createFromInstance(client, _currentWebViewClient!); return api.setWebChromeClientFromInstance(this, client); } + + /// Releases all resources used by the [WebView]. + /// + /// Any methods called after [release] will throw an exception. + Future release() { + _currentWebViewClient = null; + WebSettings.api.disposeFromInstance(settings); + return api.disposeFromInstance(this); + } } /// Manages settings state for a [WebView]. @@ -332,7 +308,7 @@ class WebSettings { /// /// The default is false. Future setSupportMultipleWindows(bool support) { - return api.setSupportZoomFromInstance(this, support); + return api.setSupportMultipleWindowsFromInstance(this, support); } /// Tells the WebView to enable JavaScript execution. @@ -423,7 +399,9 @@ class WebSettings { /// See [WebView.addJavaScriptChannel]. abstract class JavaScriptChannel { /// Constructs a [JavaScriptChannel]. - JavaScriptChannel(this.channelName); + JavaScriptChannel(this.channelName) { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } /// Pigeon Host Api implementation for [JavaScriptChannel]. @visibleForTesting @@ -433,61 +411,95 @@ abstract class JavaScriptChannel { final String channelName; /// Callback method when javaScript calls `postMessage` on the object instance passed. - void postMessage(String message) {} + void postMessage(String message); } /// Receive various notifications and requests for [WebView]. abstract class WebViewClient { /// Constructs a [WebViewClient]. - WebViewClient({this.shouldOverrideUrlLoading = true}); + WebViewClient({this.shouldOverrideUrlLoading = true}) { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } /// User authentication failed on server. - static const int errorAuthentication = 0xfffffffc; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_AUTHENTICATION + static const int errorAuthentication = -4; /// Malformed URL. - static const int errorBadUrl = 0xfffffff4; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_BAD_URL + static const int errorBadUrl = -12; /// Failed to connect to the server. - static const int errorConnect = 0xfffffffa; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_CONNECT + static const int errorConnect = -6; /// Failed to perform SSL handshake. - static const int errorFailedSslHandshake = 0xfffffff5; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FAILED_SSL_HANDSHAKE + static const int errorFailedSslHandshake = -11; /// Generic file error. - static const int errorFile = 0xfffffff3; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FILE + static const int errorFile = -13; /// File not found. - static const int errorFileNotFound = 0xfffffff2; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FILE_NOT_FOUND + static const int errorFileNotFound = -14; /// Server or proxy hostname lookup failed. - static const int errorHostLookup = 0xfffffffe; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_HOST_LOOKUP + static const int errorHostLookup = -2; /// Failed to read or write to the server. - static const int errorIO = 0xfffffff9; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_IO + static const int errorIO = -7; /// User authentication failed on proxy. - static const int errorProxyAuthentication = 0xfffffffb; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_PROXY_AUTHENTICATION + static const int errorProxyAuthentication = -5; /// Too many redirects. - static const int errorRedirectLoop = 0xfffffff7; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_REDIRECT_LOOP + static const int errorRedirectLoop = -9; /// Connection timed out. - static const int errorTimeout = 0xfffffff8; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_TIMEOUT + static const int errorTimeout = -8; /// Too many requests during this load. - static const int errorTooManyRequests = 0xfffffff1; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_TOO_MANY_REQUESTS + static const int errorTooManyRequests = -15; /// Generic error. - static const int errorUnknown = 0xffffffff; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNKNOWN + static const int errorUnknown = -1; /// Resource load was canceled by Safe Browsing. - static const int errorUnsafeResource = 0xfffffff0; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSAFE_RESOURCE + static const int errorUnsafeResource = -16; /// Unsupported authentication scheme (not basic or digest). - static const int errorUnsupportedAuthScheme = 0xfffffffd; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSUPPORTED_AUTH_SCHEME + static const int errorUnsupportedAuthScheme = -3; /// Unsupported URI scheme. - static const int errorUnsupportedScheme = 0xfffffff6; + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSUPPORTED_SCHEME + static const int errorUnsupportedScheme = -10; /// Pigeon Host Api implementation for [WebViewClient]. @visibleForTesting @@ -573,6 +585,11 @@ abstract class WebViewClient { /// The interface to be used when content can not be handled by the rendering engine for [WebView], and should be downloaded instead. abstract class DownloadListener { + /// Constructs a [DownloadListener]. + DownloadListener() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + /// Pigeon Host Api implementation for [DownloadListener]. @visibleForTesting static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); @@ -589,12 +606,17 @@ abstract class DownloadListener { /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. abstract class WebChromeClient { + /// Constructs a [WebChromeClient]. + WebChromeClient() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + /// Pigeon Host Api implementation for [WebChromeClient]. @visibleForTesting static WebChromeClientHostApiImpl api = WebChromeClientHostApiImpl(); /// Notify the host application that a file should be downloaded. - void onProgressChanged(WebView webView, int progress); + void onProgressChanged(WebView webView, int progress) {} } /// Encompasses parameters to the [WebViewClient.requestLoading] method. @@ -609,24 +631,24 @@ class WebResourceRequest { required this.requestHeaders, }); - /// Gets the URL for which the resource request was made. + /// The URL for which the resource request was made. final String url; - /// Gets whether the request was made in order to fetch the main frame's document. - final isForMainFrame; + /// Whether the request was made in order to fetch the main frame's document. + final bool isForMainFrame; - /// Gets whether the request was a result of a server-side redirect. + /// Whether the request was a result of a server-side redirect. /// /// Only supported on Android version >= 24. final bool? isRedirect; - /// Gets whether a gesture (such as a click) was associated with the request. + /// Whether a gesture (such as a click) was associated with the request. final bool hasGesture; - /// Gets the method associated with the request, for example "GET". + /// The method associated with the request, for example "GET". final String method; - /// Gets the headers associated with the request. + /// The headers associated with the request. final Map requestHeaders; } 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 81ec7c7bf80e..7fb95f1dd942 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 @@ -2,7 +2,7 @@ // 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.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.9), 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 @@ -1039,31 +1039,6 @@ class JavaScriptChannelHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.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.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { @@ -1074,11 +1049,33 @@ abstract class JavaScriptChannelFlutterApi { static const MessageCodec codec = _JavaScriptChannelFlutterApiCodec(); + void dispose(int instanceId); void postMessage(int instanceId, String message); - static void setup(JavaScriptChannelFlutterApi? api) { + static void setup(JavaScriptChannelFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.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.JavaScriptChannelFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1086,10 +1083,10 @@ abstract class JavaScriptChannelFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null int.'); - final String? arg_message = args[1] as String?; + final String? arg_message = (args[1] as String?); assert(arg_message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null String.'); api.postMessage(arg_instanceId!, arg_message!); @@ -1141,31 +1138,6 @@ class WebViewClientHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.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.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _WebViewClientFlutterApiCodec extends StandardMessageCodec { @@ -1178,9 +1150,6 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { } else if (value is WebResourceRequestData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is WebResourceRequestData) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1195,9 +1164,6 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { case 129: return WebResourceRequestData.decode(readValue(buffer)!); - case 130: - return WebResourceRequestData.decode(readValue(buffer)!); - default: return super.readValueOfType(type, buffer); } @@ -1207,6 +1173,7 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { abstract class WebViewClientFlutterApi { static const MessageCodec codec = _WebViewClientFlutterApiCodec(); + void dispose(int instanceId); void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); void onReceivedRequestError(int instanceId, int webViewInstanceId, @@ -1216,10 +1183,31 @@ abstract class WebViewClientFlutterApi { void requestLoading( int instanceId, int webViewInstanceId, WebResourceRequestData request); void urlLoading(int instanceId, int webViewInstanceId, String url); - static void setup(WebViewClientFlutterApi? api) { + static void setup(WebViewClientFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.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.WebViewClientFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1227,13 +1215,13 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); - final String? arg_url = args[2] as String?; + final String? arg_url = (args[2] as String?); assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null String.'); api.onPageStarted(arg_instanceId!, arg_webViewInstanceId!, arg_url!); @@ -1242,8 +1230,9 @@ abstract class WebViewClientFlutterApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1251,13 +1240,13 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); - final String? arg_url = args[2] as String?; + final String? arg_url = (args[2] as String?); assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null String.'); api.onPageFinished(arg_instanceId!, arg_webViewInstanceId!, arg_url!); @@ -1266,9 +1255,10 @@ abstract class WebViewClientFlutterApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1276,18 +1266,18 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); final WebResourceRequestData? arg_request = - args[2] as WebResourceRequestData?; + (args[2] as WebResourceRequestData?); assert(arg_request != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceRequestData.'); final WebResourceErrorData? arg_error = - args[3] as WebResourceErrorData?; + (args[3] as WebResourceErrorData?); assert(arg_error != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceErrorData.'); api.onReceivedRequestError(arg_instanceId!, arg_webViewInstanceId!, @@ -1297,8 +1287,9 @@ abstract class WebViewClientFlutterApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1306,19 +1297,19 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); - final int? arg_errorCode = args[2] as int?; + final int? arg_errorCode = (args[2] as int?); assert(arg_errorCode != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); - final String? arg_description = args[3] as String?; + final String? arg_description = (args[3] as String?); assert(arg_description != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); - final String? arg_failingUrl = args[4] as String?; + final String? arg_failingUrl = (args[4] as String?); assert(arg_failingUrl != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); api.onReceivedError(arg_instanceId!, arg_webViewInstanceId!, @@ -1328,8 +1319,9 @@ abstract class WebViewClientFlutterApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1337,14 +1329,14 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); final WebResourceRequestData? arg_request = - args[2] as WebResourceRequestData?; + (args[2] as WebResourceRequestData?); assert(arg_request != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null WebResourceRequestData.'); api.requestLoading( @@ -1354,8 +1346,9 @@ abstract class WebViewClientFlutterApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1363,13 +1356,13 @@ abstract class WebViewClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); - final String? arg_url = args[2] as String?; + final String? arg_url = (args[2] as String?); assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null String.'); api.urlLoading(arg_instanceId!, arg_webViewInstanceId!, arg_url!); @@ -1419,31 +1412,6 @@ class DownloadListenerHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.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.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { @@ -1453,13 +1421,35 @@ class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { abstract class DownloadListenerFlutterApi { static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + void dispose(int instanceId); void onDownloadStart(int instanceId, String url, String userAgent, String contentDisposition, String mimetype, int contentLength); - static void setup(DownloadListenerFlutterApi? api) { + static void setup(DownloadListenerFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.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.DownloadListenerFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1467,22 +1457,22 @@ abstract class DownloadListenerFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); - final String? arg_url = args[1] as String?; + final String? arg_url = (args[1] as String?); assert(arg_url != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); - final String? arg_userAgent = args[2] as String?; + final String? arg_userAgent = (args[2] as String?); assert(arg_userAgent != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); - final String? arg_contentDisposition = args[3] as String?; + final String? arg_contentDisposition = (args[3] as String?); assert(arg_contentDisposition != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); - final String? arg_mimetype = args[4] as String?; + final String? arg_mimetype = (args[4] as String?); assert(arg_mimetype != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); - final int? arg_contentLength = args[5] as int?; + final int? arg_contentLength = (args[5] as int?); assert(arg_contentLength != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); api.onDownloadStart(arg_instanceId!, arg_url!, arg_userAgent!, @@ -1535,31 +1525,6 @@ class WebChromeClientHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.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.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { @@ -1569,12 +1534,34 @@ class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { abstract class WebChromeClientFlutterApi { static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); + void dispose(int instanceId); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); - static void setup(WebChromeClientFlutterApi? api) { + static void setup(WebChromeClientFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.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.WebChromeClientFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1582,13 +1569,13 @@ abstract class WebChromeClientFlutterApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); - final int? arg_progress = args[2] as int?; + final int? arg_progress = (args[2] as int?); assert(arg_progress != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); api.onProgressChanged( 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 f909e49bd802..fba0dea0a421 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 @@ -8,6 +8,77 @@ import 'android_webview.dart'; import 'android_webview.pigeon.dart'; import 'instance_manager.dart'; +/// Converts [WebResourceRequestData] to [WebResourceRequest] +WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { + return WebResourceRequest( + url: data.url!, + isForMainFrame: data.isForMainFrame!, + isRedirect: data.isRedirect, + 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!, + ); +} + +/// Handles initialization of Flutter APIs for Android WebView. +class AndroidWebViewFlutterApis { + /// Creates a [AndroidWebViewFlutterApis]. + AndroidWebViewFlutterApis({ + DownloadListenerFlutterApiImpl? downloadListenerFlutterApi, + WebViewClientFlutterApiImpl? webViewClientFlutterApi, + WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, + JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, + }) { + this.downloadListenerFlutterApi = + downloadListenerFlutterApi ?? DownloadListenerFlutterApiImpl(); + this.webViewClientFlutterApi = + webViewClientFlutterApi ?? WebViewClientFlutterApiImpl(); + this.webChromeClientFlutterApi = + webChromeClientFlutterApi ?? WebChromeClientFlutterApiImpl(); + this.javaScriptChannelFlutterApi = + javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); + } + + static bool _haveBeenSetUp = false; + + /// Mutable instance containing all Flutter Apis for Android WebView. + /// + /// This should only be changed for testing purposes. + static AndroidWebViewFlutterApis instance = AndroidWebViewFlutterApis(); + + /// Flutter Api for [DownloadListener]. + late final DownloadListenerFlutterApiImpl downloadListenerFlutterApi; + + /// Flutter Api for [WebViewClient]. + late final WebViewClientFlutterApiImpl webViewClientFlutterApi; + + /// Flutter Api for [WebChromeClient]. + late final WebChromeClientFlutterApiImpl webChromeClientFlutterApi; + + /// Flutter Api for [JavaScriptChannel]. + late final JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi; + + /// Ensures all the Flutter APIs have been setup to receive calls from native code. + void ensureSetUp() { + if (!_haveBeenSetUp) { + DownloadListenerFlutterApi.setup(downloadListenerFlutterApi); + WebViewClientFlutterApi.setup(webViewClientFlutterApi); + WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); + JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); + _haveBeenSetUp = true; + } + } +} + /// Host api implementation for [WebView]. class WebViewHostApiImpl extends WebViewHostApi { /// Constructs a [WebViewHostApiImpl]. @@ -31,10 +102,11 @@ class WebViewHostApiImpl extends WebViewHostApi { /// Helper method to convert instances ids to objects. Future disposeFromInstance(WebView instance) async { - final int? instanceId = instanceManager.removeInstance(instance); + final int? instanceId = instanceManager.getInstanceId(instance); if (instanceId != null) { - return dispose(instanceId); + await dispose(instanceId); } + instanceManager.removeInstance(instance); } /// Helper method to convert instances ids to objects. @@ -239,7 +311,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettings instance, bool flag, ) { - return setJavaScriptCanOpenWindowsAutomatically( + return setJavaScriptEnabled( instanceManager.getInstanceId(instance)!, flag, ); @@ -337,26 +409,23 @@ class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { return create(instanceId, instance.channelName); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(JavaScriptChannel instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [JavaScriptChannel]. class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { /// Constructs a [JavaScriptChannelFlutterApiImpl]. - JavaScriptChannelHostApiImpl({InstanceManager? instanceManager}) { + JavaScriptChannelFlutterApiImpl({InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? InstanceManager.instance; } /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void postMessage(int instanceId, String message) { final JavaScriptChannel instance = @@ -385,14 +454,6 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { return create(instanceId, instance.shouldOverrideUrlLoading); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebViewClient instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [WebViewClient]. @@ -405,6 +466,11 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onPageFinished(int instanceId, int webViewInstanceId, String url) { final WebViewClient instance = @@ -455,18 +521,8 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { instanceManager.getInstance(instanceId) as WebViewClient; instance.onReceivedRequestError( instanceManager.getInstance(webViewInstanceId) as WebView, - WebResourceRequest( - url: request.url!, - isForMainFrame: request.isForMainFrame!, - isRedirect: request.isRedirect, - hasGesture: request.hasGesture!, - method: request.method!, - requestHeaders: request.requestHeaders!.cast(), - ), - WebResourceError( - errorCode: error.errorCode!, - description: error.description!, - ), + _toWebResourceRequest(request), + _toWebResourceError(error), ); } @@ -480,14 +536,7 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { instanceManager.getInstance(instanceId) as WebViewClient; instance.requestLoading( instanceManager.getInstance(webViewInstanceId) as WebView, - WebResourceRequest( - url: request.url!, - isForMainFrame: request.isForMainFrame!, - isRedirect: request.isRedirect, - hasGesture: request.hasGesture!, - method: request.method!, - requestHeaders: request.requestHeaders!.cast(), - ), + _toWebResourceRequest(request), ); } @@ -526,14 +575,6 @@ class DownloadListenerHostApiImpl extends DownloadListenerHostApi { return create(instanceId); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(DownloadListener instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [DownloadListener]. @@ -546,6 +587,11 @@ class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onDownloadStart( int instanceId, @@ -590,14 +636,6 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { return create(instanceId, instanceManager.getInstanceId(webViewClient)!); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebChromeClient instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [DownloadListener]. @@ -610,6 +648,11 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onProgressChanged(int instanceId, int webViewInstanceId, int progress) { final WebChromeClient instance = 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 1877c1b91feb..47bc42058ba9 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -105,24 +105,24 @@ abstract class WebSettingsHostApi { @HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') abstract class JavaScriptChannelHostApi { void create(int instanceId, String channelName); - - void dispose(int instanceId); } @FlutterApi() abstract class JavaScriptChannelFlutterApi { + void dispose(int instanceId); + void postMessage(int instanceId, String message); } @HostApi(dartHostTestHandler: 'TestWebViewClientHostApi') abstract class WebViewClientHostApi { void create(int instanceId, bool shouldOverrideUrlLoading); - - void dispose(int instanceId); } @FlutterApi() abstract class WebViewClientFlutterApi { + void dispose(int instanceId); + void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); @@ -154,11 +154,12 @@ abstract class WebViewClientFlutterApi { @HostApi(dartHostTestHandler: 'TestDownloadListenerHostApi') abstract class DownloadListenerHostApi { void create(int instanceId); - void dispose(int instanceId); } @FlutterApi() abstract class DownloadListenerFlutterApi { + void dispose(int instanceId); + void onDownloadStart( int instanceId, String url, @@ -172,10 +173,11 @@ abstract class DownloadListenerFlutterApi { @HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') abstract class WebChromeClientHostApi { void create(int instanceId, int webViewClientInstanceId); - void dispose(int instanceId); } @FlutterApi() abstract class WebChromeClientFlutterApi { + void dispose(int instanceId); + void onProgressChanged(int instanceId, int webViewInstanceId, int progress); } diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 9f9998e28d58..2fe03c888d6b 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -22,10 +22,12 @@ dependencies: webview_flutter_platform_interface: ^1.2.0 dev_dependencies: + build_runner: ^2.1.4 flutter_driver: sdk: flutter flutter_test: sdk: flutter - pigeon: 1.0.7 + mockito: ^5.0.16 + pigeon: 1.0.9 pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 70aa53ca2610..4173663b6248 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.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. -// Autogenerated from Pigeon (v1.0.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.9), 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, avoid_relative_lib_imports // @dart = 2.12 @@ -43,10 +43,12 @@ abstract class TestWebViewHostApi { void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); void setDownloadListener(int instanceId, int listenerInstanceId); void setWebChromeClient(int instanceId, int clientInstanceId); - static void setup(TestWebViewHostApi? api) { + static void setup(TestWebViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -54,10 +56,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null int.'); - final bool? arg_useHybridComposition = args[1] as bool?; + final bool? arg_useHybridComposition = (args[1] as bool?); assert(arg_useHybridComposition != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_useHybridComposition!); @@ -66,8 +68,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.dispose', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -75,7 +78,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); @@ -84,8 +87,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -93,14 +97,14 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null int.'); - final String? arg_url = args[1] as String?; + final String? arg_url = (args[1] as String?); assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null String.'); final Map? arg_headers = - args[2] as Map?; + (args[2] as Map?)?.cast(); assert(arg_headers != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); @@ -109,8 +113,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -118,7 +123,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + 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!); @@ -127,8 +132,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -136,7 +142,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack was null, expected non-null int.'); final bool output = api.canGoBack(arg_instanceId!); @@ -145,8 +151,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -154,7 +161,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward was null, expected non-null int.'); final bool output = api.canGoForward(arg_instanceId!); @@ -163,8 +170,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goBack', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -172,7 +180,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null, expected non-null int.'); api.goBack(arg_instanceId!); @@ -181,8 +189,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goForward', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -190,7 +199,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null, expected non-null int.'); api.goForward(arg_instanceId!); @@ -199,8 +208,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.reload', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.reload', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -208,7 +218,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null, expected non-null int.'); api.reload(arg_instanceId!); @@ -217,8 +227,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -226,10 +237,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null int.'); - final bool? arg_includeDiskFiles = args[1] as bool?; + final bool? arg_includeDiskFiles = (args[1] as bool?); assert(arg_includeDiskFiles != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); api.clearCache(arg_instanceId!, arg_includeDiskFiles!); @@ -238,8 +249,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -247,10 +259,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null int.'); - final String? arg_javascriptString = args[1] as String?; + 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( @@ -260,8 +272,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -269,7 +282,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + 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!); @@ -278,8 +291,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -287,13 +301,13 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); - final int? arg_x = args[1] as int?; + final int? arg_x = (args[1] as int?); assert(arg_x != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); - final int? arg_y = args[2] as int?; + final int? arg_y = (args[2] as int?); assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); api.scrollTo(arg_instanceId!, arg_x!, arg_y!); @@ -302,8 +316,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -311,13 +326,13 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); - final int? arg_x = args[1] as int?; + final int? arg_x = (args[1] as int?); assert(arg_x != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); - final int? arg_y = args[2] as int?; + final int? arg_y = (args[2] as int?); assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); api.scrollBy(arg_instanceId!, arg_x!, arg_y!); @@ -326,8 +341,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -335,7 +351,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX was null, expected non-null int.'); final int output = api.getScrollX(arg_instanceId!); @@ -344,8 +360,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -353,7 +370,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY was null, expected non-null int.'); final int output = api.getScrollY(arg_instanceId!); @@ -362,9 +379,10 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -372,7 +390,7 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null.'); final List args = (message as List?)!; - final bool? arg_enabled = args[0] as bool?; + final bool? arg_enabled = (args[0] as bool?); assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); api.setWebContentsDebuggingEnabled(arg_enabled!); @@ -381,8 +399,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -390,10 +409,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); - final int? arg_webViewClientInstanceId = args[1] as int?; + final int? arg_webViewClientInstanceId = (args[1] as int?); assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); @@ -402,8 +421,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -411,10 +431,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); - final int? arg_javaScriptChannelInstanceId = args[1] as int?; + final int? arg_javaScriptChannelInstanceId = (args[1] as int?); assert(arg_javaScriptChannelInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); api.addJavaScriptChannel( @@ -424,8 +444,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -433,10 +454,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); - final int? arg_javaScriptChannelInstanceId = args[1] as int?; + final int? arg_javaScriptChannelInstanceId = (args[1] as int?); assert(arg_javaScriptChannelInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); api.removeJavaScriptChannel( @@ -446,8 +467,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -455,10 +477,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); 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?; + 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!); @@ -467,8 +489,9 @@ abstract class TestWebViewHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -476,10 +499,10 @@ abstract class TestWebViewHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); 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?; + 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!); @@ -510,10 +533,12 @@ abstract class TestWebSettingsHostApi { void setUseWideViewPort(int instanceId, bool use); void setDisplayZoomControls(int instanceId, bool enabled); void setBuiltInZoomControls(int instanceId, bool enabled); - static void setup(TestWebSettingsHostApi? api) { + static void setup(TestWebSettingsHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -521,10 +546,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); - final int? arg_webViewInstanceId = args[1] as int?; + final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewInstanceId!); @@ -533,8 +558,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -542,7 +568,7 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); @@ -551,8 +577,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -560,10 +587,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null int.'); - final bool? arg_flag = args[1] as bool?; + final bool? arg_flag = (args[1] as bool?); assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); api.setDomStorageEnabled(arg_instanceId!, arg_flag!); @@ -572,9 +599,10 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -582,10 +610,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null int.'); - final bool? arg_flag = args[1] as bool?; + final bool? arg_flag = (args[1] as bool?); assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); api.setJavaScriptCanOpenWindowsAutomatically( @@ -595,9 +623,10 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -605,10 +634,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null int.'); - final bool? arg_support = args[1] as bool?; + final bool? arg_support = (args[1] as bool?); assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); api.setSupportMultipleWindows(arg_instanceId!, arg_support!); @@ -617,8 +646,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -626,10 +656,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null int.'); - final bool? arg_flag = args[1] as bool?; + final bool? arg_flag = (args[1] as bool?); assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); @@ -638,8 +668,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -647,10 +678,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); 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?; + 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!); @@ -659,9 +690,10 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -669,10 +701,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null int.'); - final bool? arg_require = args[1] as bool?; + final bool? arg_require = (args[1] as bool?); assert(arg_require != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); api.setMediaPlaybackRequiresUserGesture( @@ -682,8 +714,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -691,10 +724,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null int.'); - final bool? arg_support = args[1] as bool?; + final bool? arg_support = (args[1] as bool?); assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); api.setSupportZoom(arg_instanceId!, arg_support!); @@ -703,9 +736,10 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', - codec); + codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -713,10 +747,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null int.'); - final bool? arg_overview = args[1] as bool?; + final bool? arg_overview = (args[1] as bool?); assert(arg_overview != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); @@ -725,8 +759,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -734,10 +769,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null int.'); - final bool? arg_use = args[1] as bool?; + final bool? arg_use = (args[1] as bool?); assert(arg_use != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); api.setUseWideViewPort(arg_instanceId!, arg_use!); @@ -746,9 +781,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', - codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -756,10 +791,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null int.'); - final bool? arg_enabled = args[1] as bool?; + final bool? arg_enabled = (args[1] as bool?); assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); @@ -768,9 +803,9 @@ abstract class TestWebSettingsHostApi { } } { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', - codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -778,10 +813,10 @@ abstract class TestWebSettingsHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null int.'); - final bool? arg_enabled = args[1] as bool?; + final bool? arg_enabled = (args[1] as bool?); assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); @@ -801,11 +836,12 @@ abstract class TestJavaScriptChannelHostApi { _TestJavaScriptChannelHostApiCodec(); void create(int instanceId, String channelName); - void dispose(int instanceId); - static void setup(TestJavaScriptChannelHostApi? api) { + static void setup(TestJavaScriptChannelHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -813,10 +849,10 @@ abstract class TestJavaScriptChannelHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null int.'); - final String? arg_channelName = args[1] as String?; + final String? arg_channelName = (args[1] as String?); assert(arg_channelName != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); api.create(arg_instanceId!, arg_channelName!); @@ -824,24 +860,6 @@ abstract class TestJavaScriptChannelHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.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.JavaScriptChannelHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -853,11 +871,12 @@ abstract class TestWebViewClientHostApi { static const MessageCodec codec = _TestWebViewClientHostApiCodec(); void create(int instanceId, bool shouldOverrideUrlLoading); - void dispose(int instanceId); - static void setup(TestWebViewClientHostApi? api) { + static void setup(TestWebViewClientHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -865,10 +884,10 @@ abstract class TestWebViewClientHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null int.'); - final bool? arg_shouldOverrideUrlLoading = args[1] as bool?; + final bool? arg_shouldOverrideUrlLoading = (args[1] as bool?); assert(arg_shouldOverrideUrlLoading != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_shouldOverrideUrlLoading!); @@ -876,24 +895,6 @@ abstract class TestWebViewClientHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.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.WebViewClientHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -906,11 +907,12 @@ abstract class TestDownloadListenerHostApi { _TestDownloadListenerHostApiCodec(); void create(int instanceId); - void dispose(int instanceId); - static void setup(TestDownloadListenerHostApi? api) { + static void setup(TestDownloadListenerHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -918,7 +920,7 @@ abstract class TestDownloadListenerHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); @@ -926,24 +928,6 @@ abstract class TestDownloadListenerHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.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.DownloadListenerHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -955,11 +939,12 @@ abstract class TestWebChromeClientHostApi { static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); void create(int instanceId, int webViewClientInstanceId); - void dispose(int instanceId); - static void setup(TestWebChromeClientHostApi? api) { + static void setup(TestWebChromeClientHostApi? api, + {BinaryMessenger? binaryMessenger}) { { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec); + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { @@ -967,10 +952,10 @@ abstract class TestWebChromeClientHostApi { assert(message != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null.'); final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; + final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); - final int? arg_webViewClientInstanceId = args[1] as int?; + final int? arg_webViewClientInstanceId = (args[1] as int?); assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewClientInstanceId!); @@ -978,23 +963,5 @@ abstract class TestWebChromeClientHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.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.WebChromeClientHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } 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 462670abdb83..24cd35c9b3cf 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 @@ -3,371 +3,592 @@ // 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_android/src/android_webview.dart'; +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'; + +@GenerateMocks([ + DownloadListener, + JavaScriptChannel, + TestDownloadListenerHostApi, + TestJavaScriptChannelHostApi, + TestWebChromeClientHostApi, + TestWebSettingsHostApi, + TestWebViewClientHostApi, + TestWebViewHostApi, + WebChromeClient, + WebView, + WebViewClient, +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Android WebView', () { group('$WebView', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - }); + late MockTestWebViewHostApi mockPlatformHostApi; + + late InstanceManager instanceManager; + + late WebView webView; + late int webViewInstanceId; setUp(() { - WebView.api = WebViewHostApiImpl(instanceManager: InstanceManager()); + mockPlatformHostApi = MockTestWebViewHostApi(); + TestWebViewHostApi.setup(mockPlatformHostApi); + + instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + + webView = WebView(); + webViewInstanceId = instanceManager.getInstanceId(webView)!; }); test('create', () { - final WebView webView = WebView(); - expect(WebView.api.instanceManager.getInstanceId(webView), isNotNull); + verify(mockPlatformHostApi.create(webViewInstanceId, false)); }); - }); - group('$WebSettings', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebSettingsHostApi.setup(TestWebSettingsHostApiImpl()); + test('setWebContentsDebuggingEnabled', () { + WebView.setWebContentsDebuggingEnabled(true); + verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(true)); }); - setUp(() { - final InstanceManager instanceManager = InstanceManager(); - WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); - WebSettings.api = WebSettingsHostApiImpl( - instanceManager: instanceManager, - ); + test('loadUrl', () { + webView.loadUrl('hello', {'a': 'header'}); + verify(mockPlatformHostApi.loadUrl( + webViewInstanceId, + 'hello', + {'a': 'header'}, + )); }); - test('create', () { - final WebView webView = WebView(); - final WebSettings webSettings = WebSettings(webView); - expect( - WebSettings.api.instanceManager.getInstanceId(webSettings), - isNotNull, - ); + test('canGoBack', () { + when(mockPlatformHostApi.canGoBack(webViewInstanceId)) + .thenReturn(false); + expect(webView.canGoBack(), completion(false)); }); - }); - group('$JavaScriptChannel', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestJavaScriptChannelHostApi.setup(TestJavaScriptChannelHostApiImpl()); + test('canGoForward', () { + when(mockPlatformHostApi.canGoForward(webViewInstanceId)) + .thenReturn(true); + expect(webView.canGoForward(), completion(true)); }); - setUp(() { - final InstanceManager instanceManager = InstanceManager(); - WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); - JavaScriptChannel.api = JavaScriptChannelHostApiImpl( - instanceManager: instanceManager, - ); + test('goBack', () { + webView.goBack(); + verify(mockPlatformHostApi.goBack(webViewInstanceId)); }); - test('create', () { - final WebView webView = WebView(); - final JavaScriptChannel channel = TestJavaScriptChannel('myChannel'); + test('goForward', () { + webView.goForward(); + verify(mockPlatformHostApi.goForward(webViewInstanceId)); + }); - webView.addJavaScriptChannel(channel); - expect( - JavaScriptChannel.api.instanceManager.getInstanceId(channel), - isNotNull, - ); + test('reload', () { + webView.reload(); + verify(mockPlatformHostApi.reload(webViewInstanceId)); + }); + + test('clearCache', () { + webView.clearCache(false); + verify(mockPlatformHostApi.clearCache(webViewInstanceId, false)); + }); - webView.removeJavaScriptChannel(channel); + test('evaluateJavascript', () { + when( + mockPlatformHostApi.evaluateJavascript( + webViewInstanceId, 'runJavaScript'), + ).thenAnswer((_) => Future.value('returnValue')); expect( - JavaScriptChannel.api.instanceManager.getInstanceId(channel), - isNull, + webView.evaluateJavascript('runJavaScript'), + completion('returnValue'), ); }); - }); - group('$WebViewClient', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); + test('getTitle', () { + when(mockPlatformHostApi.getTitle(webViewInstanceId)) + .thenReturn('aTitle'); + expect(webView.getTitle(), completion('aTitle')); }); - setUp(() { - final InstanceManager instanceManager = InstanceManager(); - WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + test('scrollTo', () { + webView.scrollTo(12, 13); + verify(mockPlatformHostApi.scrollTo(webViewInstanceId, 12, 13)); + }); + + test('scrollBy', () { + webView.scrollBy(12, 14); + verify(mockPlatformHostApi.scrollBy(webViewInstanceId, 12, 14)); + }); + + test('getScrollX', () { + when(mockPlatformHostApi.getScrollX(webViewInstanceId)).thenReturn(67); + expect(webView.getScrollX(), completion(67)); + }); + + test('getScrollY', () { + when(mockPlatformHostApi.getScrollY(webViewInstanceId)).thenReturn(56); + expect(webView.getScrollY(), completion(56)); + }); + + test('setWebViewClient', () { + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); WebViewClient.api = WebViewClientHostApiImpl( instanceManager: instanceManager, ); - }); - test('create', () { - final WebView webView = WebView(); - final WebViewClient webViewClient1 = TestWebViewClient(); - final WebViewClient webViewClient2 = TestWebViewClient(); + final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); + webView.setWebViewClient(mockWebViewClient); - webView.setWebViewClient(webViewClient1); - expect( - WebViewClient.api.instanceManager.getInstanceId(webViewClient1), - isNotNull, - ); + final int webViewClientInstanceId = + instanceManager.getInstanceId(mockWebViewClient)!; + verify(mockPlatformHostApi.setWebViewClient( + webViewInstanceId, + webViewClientInstanceId, + )); + }); - webView.setWebViewClient(webViewClient2); - expect( - WebViewClient.api.instanceManager.getInstanceId(webViewClient1), - isNull, + test('addJavaScriptChannel', () { + TestJavaScriptChannelHostApi.setup(MockTestJavaScriptChannelHostApi()); + JavaScriptChannel.api = JavaScriptChannelHostApiImpl( + instanceManager: instanceManager, ); - }); - }); - group('$DownloadListener', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestDownloadListenerHostApi.setup(TestDownloadListenerHostApiImpl()); + final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); + + webView.addJavaScriptChannel(mockJavaScriptChannel); + + final int javaScriptChannelInstanceId = + instanceManager.getInstanceId(mockJavaScriptChannel)!; + verify(mockPlatformHostApi.addJavaScriptChannel( + webViewInstanceId, + javaScriptChannelInstanceId, + )); }); - setUp(() { - final InstanceManager instanceManager = InstanceManager(); - WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); - DownloadListener.api = DownloadListenerHostApiImpl( + test('removeJavaScriptChannel', () { + TestJavaScriptChannelHostApi.setup(MockTestJavaScriptChannelHostApi()); + JavaScriptChannel.api = JavaScriptChannelHostApiImpl( instanceManager: instanceManager, ); - }); - test('create', () { - final WebView webView = WebView(); - final DownloadListener downloadListener1 = TestDownloadListener(); - final DownloadListener downloadListener2 = TestDownloadListener(); + final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); - webView.setDownloadListener(downloadListener1); expect( - DownloadListener.api.instanceManager.getInstanceId(downloadListener1), - isNotNull, + webView.removeJavaScriptChannel(mockJavaScriptChannel), + completes, ); - webView.setDownloadListener(downloadListener2); - expect( - DownloadListener.api.instanceManager.getInstanceId(downloadListener1), - isNull, - ); + webView.addJavaScriptChannel(mockJavaScriptChannel); + webView.removeJavaScriptChannel(mockJavaScriptChannel); + + final int javaScriptChannelInstanceId = + instanceManager.getInstanceId(mockJavaScriptChannel)!; + verify(mockPlatformHostApi.removeJavaScriptChannel( + webViewInstanceId, + javaScriptChannelInstanceId, + )); }); - }); - group('$WebChromeClient', () { - setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); - TestWebChromeClientHostApi.setup(TestWebChromeClientHostApiImpl()); + test('setDownloadListener', () { + TestDownloadListenerHostApi.setup(MockTestDownloadListenerHostApi()); + DownloadListener.api = DownloadListenerHostApiImpl( + instanceManager: instanceManager, + ); + + final DownloadListener mockDownloadListener = MockDownloadListener(); + webView.setDownloadListener(mockDownloadListener); + + final int downloadListenerInstanceId = + instanceManager.getInstanceId(mockDownloadListener)!; + verify(mockPlatformHostApi.setDownloadListener( + webViewInstanceId, + downloadListenerInstanceId, + )); }); - setUp(() { - final InstanceManager instanceManager = InstanceManager(); - WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + test('setWebChromeClient', () { + // Setting a WebChromeClient requires setting a WebViewClient first. + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); WebViewClient.api = WebViewClientHostApiImpl( instanceManager: instanceManager, ); + final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); + webView.setWebViewClient(mockWebViewClient); + + TestWebChromeClientHostApi.setup(MockTestWebChromeClientHostApi()); WebChromeClient.api = WebChromeClientHostApiImpl( instanceManager: instanceManager, ); + + final WebChromeClient mockWebChromeClient = MockWebChromeClient(); + webView.setWebChromeClient(mockWebChromeClient); + + final int webChromeClientInstanceId = + instanceManager.getInstanceId(mockWebChromeClient)!; + verify(mockPlatformHostApi.setWebChromeClient( + webViewInstanceId, + webChromeClientInstanceId, + )); }); - test('create', () { - final WebView webView = WebView(); - webView.setWebViewClient(TestWebViewClient()); + test('release', () { + final MockTestWebSettingsHostApi mockWebSettingsPlatformHostApi = + MockTestWebSettingsHostApi(); + TestWebSettingsHostApi.setup(mockWebSettingsPlatformHostApi); - final WebChromeClient webChromeClient1 = TestWebChromeClient(); - final WebChromeClient webChromeClient2 = TestWebChromeClient(); + WebSettings.api = + WebSettingsHostApiImpl(instanceManager: instanceManager); + final int webSettingsInstanceId = + instanceManager.getInstanceId(webView.settings)!; - webView.setWebChromeClient(webChromeClient1); - expect( - WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), - isNotNull, - ); + webView.release(); + verify(mockWebSettingsPlatformHostApi.dispose(webSettingsInstanceId)); + verify(mockPlatformHostApi.dispose(webViewInstanceId)); + }); + }); - webView.setWebChromeClient(webChromeClient2); - expect( - WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), - isNull, + group('$WebSettings', () { + late MockTestWebSettingsHostApi mockPlatformHostApi; + + late InstanceManager instanceManager; + + late WebSettings webSettings; + late int webSettingsInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + + mockPlatformHostApi = MockTestWebSettingsHostApi(); + TestWebSettingsHostApi.setup(mockPlatformHostApi); + + WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, ); + + webSettings = WebSettings(WebView()); + webSettingsInstanceId = instanceManager.getInstanceId(webSettings)!; }); - }); - }); -} -class TestJavaScriptChannel extends JavaScriptChannel { - TestJavaScriptChannel(String channelName) : super(channelName); + test('create', () { + verify(mockPlatformHostApi.create(webSettingsInstanceId, any)); + }); - @override - void postMessage(String message) {} -} + test('setDomStorageEnabled', () { + webSettings.setDomStorageEnabled(false); + verify(mockPlatformHostApi.setDomStorageEnabled( + webSettingsInstanceId, + false, + )); + }); -class TestWebViewClient extends WebViewClient {} - -class TestDownloadListener extends DownloadListener { - @override - void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - int contentLength, - ) {} -} + test('setJavaScriptCanOpenWindowsAutomatically', () { + webSettings.setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockPlatformHostApi.setJavaScriptCanOpenWindowsAutomatically( + webSettingsInstanceId, + true, + )); + }); -class TestWebChromeClient extends WebChromeClient { - @override - void onProgressChanged(WebView webView, int progress) {} -} + test('setSupportMultipleWindows', () { + webSettings.setSupportMultipleWindows(false); + verify(mockPlatformHostApi.setSupportMultipleWindows( + webSettingsInstanceId, + false, + )); + }); + + test('setJavaScriptEnabled', () { + webSettings.setJavaScriptEnabled(true); + verify(mockPlatformHostApi.setJavaScriptEnabled( + webSettingsInstanceId, + true, + )); + }); -class TestWebViewHostApiImpl extends TestWebViewHostApi { - @override - void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId) {} + test('setUserAgentString', () { + webSettings.setUserAgentString('hola'); + verify(mockPlatformHostApi.setUserAgentString( + webSettingsInstanceId, + 'hola', + )); + }); - @override - bool canGoBack(int instanceId) { - throw UnimplementedError(); - } + test('setMediaPlaybackRequiresUserGesture', () { + webSettings.setMediaPlaybackRequiresUserGesture(false); + verify(mockPlatformHostApi.setMediaPlaybackRequiresUserGesture( + webSettingsInstanceId, + false, + )); + }); - @override - bool canGoForward(int instanceId) { - throw UnimplementedError(); - } + test('setSupportZoom', () { + webSettings.setSupportZoom(true); + verify(mockPlatformHostApi.setSupportZoom( + webSettingsInstanceId, + true, + )); + }); + + test('setLoadWithOverviewMode', () { + webSettings.setLoadWithOverviewMode(false); + verify(mockPlatformHostApi.setLoadWithOverviewMode( + webSettingsInstanceId, + false, + )); + }); - @override - void clearCache(int instanceId, bool includeDiskFiles) {} + test('setUseWideViewPort', () { + webSettings.setUseWideViewPort(true); + verify(mockPlatformHostApi.setUseWideViewPort( + webSettingsInstanceId, + true, + )); + }); - @override - void create(int instanceId, bool useHybridComposition) {} + test('setDisplayZoomControls', () { + webSettings.setDisplayZoomControls(false); + verify(mockPlatformHostApi.setDisplayZoomControls( + webSettingsInstanceId, + false, + )); + }); - @override - void dispose(int instanceId) {} + test('setBuiltInZoomControls', () { + webSettings.setBuiltInZoomControls(true); + verify(mockPlatformHostApi.setBuiltInZoomControls( + webSettingsInstanceId, + true, + )); + }); + }); - @override - Future evaluateJavascript(int instanceId, String javascriptString) { - throw UnimplementedError(); - } + group('$JavaScriptChannel', () { + late JavaScriptChannelFlutterApiImpl flutterApi; - @override - int getScrollX(int instanceId) { - throw UnimplementedError(); - } + late InstanceManager instanceManager; - @override - int getScrollY(int instanceId) { - throw UnimplementedError(); - } + late MockJavaScriptChannel mockJavaScriptChannel; + late int mockJavaScriptChannelInstanceId; - @override - String getTitle(int instanceId) { - throw UnimplementedError(); - } + setUp(() { + instanceManager = InstanceManager(); + flutterApi = JavaScriptChannelFlutterApiImpl( + instanceManager: instanceManager, + ); - @override - String getUrl(int instanceId) { - throw UnimplementedError(); - } + mockJavaScriptChannel = MockJavaScriptChannel(); + mockJavaScriptChannelInstanceId = + instanceManager.tryAddInstance(mockJavaScriptChannel)!; + }); - @override - void goBack(int instanceId) {} + test('postMessage', () { + flutterApi.postMessage( + mockJavaScriptChannelInstanceId, + 'Hello, World!', + ); + verify(mockJavaScriptChannel.postMessage('Hello, World!')); + }); + }); - @override - void goForward(int instanceId) {} + group('$WebViewClient', () { + late WebViewClientFlutterApiImpl flutterApi; - @override - void loadUrl(int instanceId, String url, Map headers) {} + late InstanceManager instanceManager; - @override - void reload(int instanceId) {} + late MockWebViewClient mockWebViewClient; + late int mockWebViewClientInstanceId; - @override - void removeJavaScriptChannel( - int instanceId, int javaScriptChannelInstanceId) {} + late MockWebView mockWebView; + late int mockWebViewInstanceId; - @override - void scrollBy(int instanceId, int x, int y) {} + setUp(() { + instanceManager = InstanceManager(); + flutterApi = WebViewClientFlutterApiImpl( + instanceManager: instanceManager, + ); - @override - void scrollTo(int instanceId, int x, int y) {} + mockWebViewClient = MockWebViewClient(); + mockWebViewClientInstanceId = + instanceManager.tryAddInstance(mockWebViewClient)!; - @override - void setDownloadListener(int instanceId, int listenerInstanceId) {} + mockWebView = MockWebView(); + mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + }); - @override - void setWebContentsDebuggingEnabled(bool enabled) {} + test('onPageStarted', () { + flutterApi.onPageStarted( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 'https://www.google.com', + ); + verify(mockWebViewClient.onPageStarted( + mockWebView, + 'https://www.google.com', + )); + }); - @override - void setWebViewClient(int instanceId, int webViewClientInstanceId) {} + test('onPageFinished', () { + flutterApi.onPageFinished( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 'https://www.google.com', + ); + verify(mockWebViewClient.onPageFinished( + mockWebView, + 'https://www.google.com', + )); + }); - @override - void setWebChromeClient(int instanceId, int clientInstanceId) {} -} + test('onReceivedRequestError', () { + flutterApi.onReceivedRequestError( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + WebResourceRequestData() + ..url = 'https://www.google.com' + ..isForMainFrame = true + ..hasGesture = true + ..method = 'POST', + WebResourceErrorData() + ..errorCode = 34 + ..description = 'error description', + ); -class TestWebSettingsHostApiImpl extends TestWebSettingsHostApi { - @override - void create(int instanceId, int webViewInstanceId) {} + verify(mockWebViewClient.onReceivedRequestError( + mockWebView, + argThat(isNotNull), + argThat(isNotNull), + )); + }); - @override - void dispose(int instanceId) {} + test('onReceivedError', () { + flutterApi.onReceivedError( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 14, + 'desc', + 'https://www.google.com', + ); - @override - void setBuiltInZoomControls(int instanceId, bool enabled) {} + verify(mockWebViewClient.onReceivedError( + mockWebView, + 14, + 'desc', + 'https://www.google.com', + )); + }); - @override - void setDisplayZoomControls(int instanceId, bool enabled) {} + test('requestLoading', () { + flutterApi.requestLoading( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + WebResourceRequestData() + ..url = 'https://www.google.com' + ..isForMainFrame = true + ..hasGesture = true + ..method = 'POST', + ); - @override - void setDomStorageEnabled(int instanceId, bool flag) {} + verify(mockWebViewClient.requestLoading( + mockWebView, + argThat(isNotNull), + )); + }); - @override - void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag) {} + test('urlLoading', () { + flutterApi.urlLoading(mockWebViewClientInstanceId, + mockWebViewInstanceId, 'https://www.google.com'); - @override - void setJavaScriptEnabled(int instanceId, bool flag) {} + verify(mockWebViewClient.urlLoading( + mockWebView, + 'https://www.google.com', + )); + }); + }); - @override - void setLoadWithOverviewMode(int instanceId, bool overview) {} + group('$DownloadListener', () { + late DownloadListenerFlutterApiImpl flutterApi; - @override - void setMediaPlaybackRequiresUserGesture(int instanceId, bool require) {} + late InstanceManager instanceManager; - @override - void setSupportMultipleWindows(int instanceId, bool support) {} + late MockDownloadListener mockDownloadListener; + late int mockDownloadListenerInstanceId; - @override - void setSupportZoom(int instanceId, bool support) {} + setUp(() { + instanceManager = InstanceManager(); + flutterApi = DownloadListenerFlutterApiImpl( + instanceManager: instanceManager, + ); - @override - void setUseWideViewPort(int instanceId, bool use) {} + mockDownloadListener = MockDownloadListener(); + mockDownloadListenerInstanceId = + instanceManager.tryAddInstance(mockDownloadListener)!; + }); - @override - void setUserAgentString(int instanceId, String userAgentString) {} -} + test('onPageStarted', () { + flutterApi.onDownloadStart( + mockDownloadListenerInstanceId, + 'url', + 'userAgent', + 'contentDescription', + 'mimetype', + 45, + ); + verify(mockDownloadListener.onDownloadStart( + 'url', + 'userAgent', + 'contentDescription', + 'mimetype', + 45, + )); + }); + }); -class TestJavaScriptChannelHostApiImpl extends TestJavaScriptChannelHostApi { - @override - void create(int instanceId, String channelName) {} + group('$WebChromeClient', () { + late WebChromeClientFlutterApiImpl flutterApi; - @override - void dispose(int instanceId) {} -} + late InstanceManager instanceManager; -class TestWebViewClientHostApiImpl extends TestWebViewClientHostApi { - @override - void create(int instanceId, bool shouldOverrideUrlLoading) {} + late MockWebChromeClient mockWebChromeClient; + late int mockWebChromeClientInstanceId; - @override - void dispose(int instanceId) {} -} + late MockWebView mockWebView; + late int mockWebViewInstanceId; -class TestDownloadListenerHostApiImpl extends TestDownloadListenerHostApi { - @override - void create(int instanceId) {} + setUp(() { + instanceManager = InstanceManager(); + flutterApi = WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); - @override - void dispose(int instanceId) {} -} + mockWebChromeClient = MockWebChromeClient(); + mockWebChromeClientInstanceId = + instanceManager.tryAddInstance(mockWebChromeClient)!; -class TestWebChromeClientHostApiImpl extends TestWebChromeClientHostApi { - @override - void create(int instanceId, int webViewClientInstanceId) {} + mockWebView = MockWebView(); + mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + }); - @override - void dispose(int instanceId) {} + test('onPageStarted', () { + flutterApi.onProgressChanged( + mockWebChromeClientInstanceId, + mockWebViewInstanceId, + 76, + ); + verify(mockWebChromeClient.onProgressChanged(mockWebView, 76)); + }); + }); + }); } 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 new file mode 100644 index 000000000000..4378ed7abf77 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -0,0 +1,505 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/android_webview_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; + +import 'android_webview.pigeon.dart' as _i3; + +// 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 _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} + +/// A class which mocks [DownloadListener]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDownloadListener extends _i1.Mock implements _i2.DownloadListener { + MockDownloadListener() { + _i1.throwOnMissingStub(this); + } + + @override + void onDownloadStart(String? url, String? userAgent, + String? contentDisposition, String? mimetype, int? contentLength) => + super.noSuchMethod( + Invocation.method(#onDownloadStart, + [url, userAgent, contentDisposition, mimetype, contentLength]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [JavaScriptChannel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { + MockJavaScriptChannel() { + _i1.throwOnMissingStub(this); + } + + @override + String get channelName => + (super.noSuchMethod(Invocation.getter(#channelName), returnValue: '') + as String); + @override + void postMessage(String? message) => + super.noSuchMethod(Invocation.method(#postMessage, [message]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestDownloadListenerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestDownloadListenerHostApi extends _i1.Mock + implements _i3.TestDownloadListenerHostApi { + MockTestDownloadListenerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestJavaScriptChannelHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestJavaScriptChannelHostApi extends _i1.Mock + implements _i3.TestJavaScriptChannelHostApi { + MockTestJavaScriptChannelHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, String? channelName) => + super.noSuchMethod(Invocation.method(#create, [instanceId, channelName]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebChromeClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebChromeClientHostApi extends _i1.Mock + implements _i3.TestWebChromeClientHostApi { + MockTestWebChromeClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebSettingsHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebSettingsHostApi extends _i1.Mock + implements _i3.TestWebSettingsHostApi { + MockTestWebSettingsHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewInstanceId) => super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void setDomStorageEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setDomStorageEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setJavaScriptCanOpenWindowsAutomatically(int? instanceId, bool? flag) => + super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setSupportMultipleWindows(int? instanceId, bool? support) => + super.noSuchMethod( + Invocation.method(#setSupportMultipleWindows, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setJavaScriptEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setJavaScriptEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setUserAgentString(int? instanceId, String? userAgentString) => + super.noSuchMethod( + Invocation.method(#setUserAgentString, [instanceId, userAgentString]), + returnValueForMissingStub: null); + @override + void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, [instanceId, require]), + returnValueForMissingStub: null); + @override + void setSupportZoom(int? instanceId, bool? support) => super.noSuchMethod( + Invocation.method(#setSupportZoom, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setLoadWithOverviewMode(int? instanceId, bool? overview) => + super.noSuchMethod( + Invocation.method(#setLoadWithOverviewMode, [instanceId, overview]), + returnValueForMissingStub: null); + @override + void setUseWideViewPort(int? instanceId, bool? use) => super.noSuchMethod( + Invocation.method(#setUseWideViewPort, [instanceId, use]), + returnValueForMissingStub: null); + @override + void setDisplayZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setDisplayZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + void setBuiltInZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebViewClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewClientHostApi extends _i1.Mock + implements _i3.TestWebViewClientHostApi { + MockTestWebViewClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? shouldOverrideUrlLoading) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, shouldOverrideUrlLoading]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewHostApi extends _i1.Mock + implements _i3.TestWebViewHostApi { + MockTestWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? useHybridComposition) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, useHybridComposition]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void loadUrl(int? instanceId, String? url, Map? headers) => + super.noSuchMethod( + Invocation.method(#loadUrl, [instanceId, url, headers]), + returnValueForMissingStub: null); + @override + String getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), + returnValue: '') as String); + @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 + void clearCache(int? instanceId, bool? includeDiskFiles) => + super.noSuchMethod( + Invocation.method(#clearCache, [instanceId, includeDiskFiles]), + returnValueForMissingStub: null); + @override + _i4.Future evaluateJavascript( + int? instanceId, String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, [instanceId, javascriptString]), + returnValue: Future.value('')) as _i4.Future); + @override + String getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), + returnValue: '') as String); + @override + void scrollTo(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + void scrollBy(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + int getScrollX(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollX, [instanceId]), + returnValue: 0) as int); + @override + int getScrollY(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollY, [instanceId]), + returnValue: 0) as int); + @override + void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( + Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), + returnValueForMissingStub: null); + @override + void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebViewClient, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + void addJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void removeJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, + [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void setDownloadListener(int? instanceId, int? listenerInstanceId) => + super.noSuchMethod( + Invocation.method( + #setDownloadListener, [instanceId, listenerInstanceId]), + returnValueForMissingStub: null); + @override + void setWebChromeClient(int? instanceId, int? clientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebChromeClient, [instanceId, clientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebChromeClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { + MockWebChromeClient() { + _i1.throwOnMissingStub(this); + } + + @override + void onProgressChanged(_i2.WebView? webView, int? progress) => super + .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + MockWebView() { + _i1.throwOnMissingStub(this); + } + + @override + bool get useHybridComposition => + (super.noSuchMethod(Invocation.getter(#useHybridComposition), + returnValue: false) as bool); + @override + _i2.WebSettings get settings => + (super.noSuchMethod(Invocation.getter(#settings), + returnValue: _FakeWebSettings_0()) as _i2.WebSettings); + @override + _i4.Future loadUrl(String? url, Map? headers) => + (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future getUrl() => + (super.noSuchMethod(Invocation.method(#getUrl, []), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future clearCache(bool? includeDiskFiles) => + (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future evaluateJavascript(String? javascriptString) => (super + .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future scrollTo(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future scrollBy(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future getScrollX() => + (super.noSuchMethod(Invocation.method(#getScrollX, []), + returnValue: Future.value(0)) as _i4.Future); + @override + _i4.Future getScrollY() => + (super.noSuchMethod(Invocation.method(#getScrollY, []), + returnValue: Future.value(0)) as _i4.Future); + @override + _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future addJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future removeJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future release() => + (super.noSuchMethod(Invocation.method(#release, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { + MockWebViewClient() { + _i1.throwOnMissingStub(this); + } + + @override + bool get shouldOverrideUrlLoading => + (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), + returnValue: false) as bool); + @override + void onPageStarted(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), + returnValueForMissingStub: null); + @override + void onPageFinished(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), + returnValueForMissingStub: null); + @override + void onReceivedRequestError(_i2.WebView? webView, + _i2.WebResourceRequest? request, _i2.WebResourceError? error) => + super.noSuchMethod( + Invocation.method(#onReceivedRequestError, [webView, request, error]), + returnValueForMissingStub: null); + @override + void onReceivedError(_i2.WebView? webView, int? errorCode, + String? description, String? failingUrl) => + super.noSuchMethod( + Invocation.method( + #onReceivedError, [webView, errorCode, description, failingUrl]), + returnValueForMissingStub: null); + @override + void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => + super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), + returnValueForMissingStub: null); + @override + void urlLoading(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} From d09abd5263758b875cef618113a3c045f0f67f4f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 10 Nov 2021 16:51:59 -0800 Subject: [PATCH 155/259] [webview_flutter_android] Fix bugs in Java Implementation of the Pigeon API (#4459) --- .../DownloadListenerFlutterApiImpl.java | 64 +++ .../DownloadListenerHostApiImpl.java | 90 ++++- .../GeneratedAndroidWebView.java | 162 +++----- .../webviewflutter/InstanceManager.java | 63 ++- .../webviewflutter/JavaScriptChannel.java | 75 +++- .../JavaScriptChannelFlutterApiImpl.java | 50 +++ .../JavaScriptChannelHostApiImpl.java | 62 +-- .../plugins/webviewflutter/Releasable.java | 14 + .../WebChromeClientFlutterApiImpl.java | 56 +++ .../WebChromeClientHostApiImpl.java | 167 +++++--- .../WebSettingsHostApiImpl.java | 32 +- .../WebViewClientFlutterApiImpl.java | 198 +++++++++ .../WebViewClientHostApiImpl.java | 376 +++++++++--------- .../webviewflutter/WebViewHostApiImpl.java | 272 ++++++++++++- .../webviewflutter/DownloadListenerTest.java | 40 +- .../webviewflutter/JavaScriptChannelTest.java | 39 +- .../webviewflutter/WebChromeClientTest.java | 46 ++- .../webviewflutter/WebViewClientTest.java | 72 ++-- .../plugins/webviewflutter/WebViewTest.java | 164 ++++++-- 19 files changed, 1467 insertions(+), 575 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java new file mode 100644 index 000000000000..2dd98c47d582 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java @@ -0,0 +1,64 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; + +/** + * Flutter Api implementation for {@link DownloadListener}. + * + *

Passes arguments of callbacks methods from a {@link DownloadListener} to Dart. + */ +public class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public DownloadListenerFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link DownloadListener#onDownloadStart} to Dart. */ + public void onDownloadStart( + DownloadListener downloadListener, + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength, + Reply callback) { + onDownloadStart( + instanceManager.getInstanceId(downloadListener), + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link DownloadListener} was removed. + * + * @param downloadListener the instance whose reference will be removed + * @param callback reply callback with return value from Dart + */ + public void dispose(DownloadListener downloadListener, Reply callback) { + final Long instanceId = instanceManager.removeInstance(downloadListener); + if (instanceId != null) { + dispose(instanceId, callback); + } else { + callback.reply(null); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java index 202be87d7d1e..9694f396ad2e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -5,40 +5,92 @@ package io.flutter.plugins.webviewflutter; import android.webkit.DownloadListener; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; -class DownloadListenerHostApiImpl implements GeneratedAndroidWebView.DownloadListenerHostApi { +/** + * Host api implementation for {@link DownloadListener}. + * + *

Handles creating {@link DownloadListener}s that intercommunicate with a paired Dart object. + */ +public class DownloadListenerHostApiImpl implements DownloadListenerHostApi { private final InstanceManager instanceManager; private final DownloadListenerCreator downloadListenerCreator; - private final GeneratedAndroidWebView.DownloadListenerFlutterApi downloadListenerFlutterApi; - - static class DownloadListenerCreator { - DownloadListener createDownloadListener( - Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { - return (url, userAgent, contentDisposition, mimetype, contentLength) -> - downloadListenerFlutterApi.onDownloadStart( - instanceId, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + private final DownloadListenerFlutterApiImpl flutterApi; + + /** + * Implementation of {@link DownloadListener} that passes arguments of callback methods to Dart. + * + *

No messages are sent to Dart after {@link DownloadListenerImpl#release} is called. + */ + public static class DownloadListenerImpl implements DownloadListener, Releasable { + @Nullable private DownloadListenerFlutterApiImpl flutterApi; + + /** + * Creates a {@link DownloadListenerImpl} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + */ + public DownloadListenerImpl(@NonNull DownloadListenerFlutterApiImpl flutterApi) { + this.flutterApi = flutterApi; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + if (flutterApi != null) { + flutterApi.onDownloadStart( + this, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + } + } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; } } - DownloadListenerHostApiImpl( + /** Handles creating {@link DownloadListenerImpl}s for a {@link DownloadListenerHostApiImpl}. */ + public static class DownloadListenerCreator { + /** + * Creates a {@link DownloadListenerImpl}. + * + * @param flutterApi handles sending messages to Dart + * @return the created {@link DownloadListenerImpl} + */ + public DownloadListenerImpl createDownloadListener(DownloadListenerFlutterApiImpl flutterApi) { + return new DownloadListenerImpl(flutterApi); + } + } + + /** + * Creates a host API that handles creating {@link DownloadListener}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param downloadListenerCreator handles creating {@link DownloadListenerImpl}s + * @param flutterApi handles sending messages to Dart + */ + public DownloadListenerHostApiImpl( InstanceManager instanceManager, DownloadListenerCreator downloadListenerCreator, - DownloadListenerFlutterApi downloadListenerFlutterApi) { + DownloadListenerFlutterApiImpl flutterApi) { this.instanceManager = instanceManager; this.downloadListenerCreator = downloadListenerCreator; - this.downloadListenerFlutterApi = downloadListenerFlutterApi; + this.flutterApi = flutterApi; } @Override public void create(Long instanceId) { final DownloadListener downloadListener = - downloadListenerCreator.createDownloadListener(instanceId, downloadListenerFlutterApi); + downloadListenerCreator.createDownloadListener(flutterApi); instanceManager.addInstance(downloadListener, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } 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 ba2b9b1ac481..a34274893117 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 @@ -2,7 +2,7 @@ // 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.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.9), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -1313,8 +1313,6 @@ private JavaScriptChannelHostApiCodec() {} public interface JavaScriptChannelHostApi { void create(Long instanceId, String channelName); - void dispose(Long instanceId); - /** The codec used by JavaScriptChannelHostApi. */ static MessageCodec getCodec() { return JavaScriptChannelHostApiCodec.INSTANCE; @@ -1354,31 +1352,6 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelHostApi.dispose", 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.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1405,6 +1378,19 @@ static MessageCodec getCodec() { return JavaScriptChannelFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void postMessage(Long instanceIdArg, String messageArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1429,8 +1415,6 @@ private WebViewClientHostApiCodec() {} public interface WebViewClientHostApi { void create(Long instanceId, Boolean shouldOverrideUrlLoading); - void dispose(Long instanceId); - /** The codec used by WebViewClientHostApi. */ static MessageCodec getCodec() { return WebViewClientHostApiCodec.INSTANCE; @@ -1471,31 +1455,6 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebViewClientHostApi.dispose", 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.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1513,9 +1472,6 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { case (byte) 129: return WebResourceRequestData.fromMap((Map) readValue(buffer)); - case (byte) 130: - return WebResourceRequestData.fromMap((Map) readValue(buffer)); - default: return super.readValueOfType(type, buffer); } @@ -1529,9 +1485,6 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { } else if (value instanceof WebResourceRequestData) { stream.write(129); writeValue(stream, ((WebResourceRequestData) value).toMap()); - } else if (value instanceof WebResourceRequestData) { - stream.write(130); - writeValue(stream, ((WebResourceRequestData) value).toMap()); } else { super.writeValue(stream, value); } @@ -1554,6 +1507,17 @@ static MessageCodec getCodec() { return WebViewClientFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onPageStarted( Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { BasicMessageChannel channel = @@ -1666,8 +1630,6 @@ private DownloadListenerHostApiCodec() {} public interface DownloadListenerHostApi { void create(Long instanceId); - void dispose(Long instanceId); - /** The codec used by DownloadListenerHostApi. */ static MessageCodec getCodec() { return DownloadListenerHostApiCodec.INSTANCE; @@ -1703,31 +1665,6 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.DownloadListenerHostApi.dispose", 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.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1754,6 +1691,17 @@ static MessageCodec getCodec() { return DownloadListenerFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onDownloadStart( Long instanceIdArg, String urlArg, @@ -1792,8 +1740,6 @@ private WebChromeClientHostApiCodec() {} public interface WebChromeClientHostApi { void create(Long instanceId, Long webViewClientInstanceId); - void dispose(Long instanceId); - /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { return WebChromeClientHostApiCodec.INSTANCE; @@ -1833,31 +1779,6 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebChromeClientHostApi.dispose", 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.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1884,6 +1805,17 @@ static MessageCodec getCodec() { return WebChromeClientFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onProgressChanged( Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply callback) { BasicMessageChannel channel = diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java index bfa7d6f17345..a368baf266dd 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -8,32 +8,77 @@ import java.util.HashMap; import java.util.Map; -class InstanceManager { +/** + * Maintains instances to intercommunicate with Dart objects. + * + *

When an instance is added with an instanceId, either can be used to retrieve the other. + */ +public class InstanceManager { private final LongSparseArray instanceIdsToInstances = new LongSparseArray<>(); private final Map instancesToInstanceIds = new HashMap<>(); - /** Add a new instance with instanceId. */ - void addInstance(Object instance, long instanceId) { + /** + * Add a new instance to the manager. + * + *

If an instance or instanceId has already been added, it will be replaced by the new values. + * + * @param instance the new object to be added + * @param instanceId unique id of the added object + */ + public void addInstance(Object instance, long instanceId) { instancesToInstanceIds.put(instance, instanceId); instanceIdsToInstances.append(instanceId, instance); } - /** Remove the instance from the manager. */ - void removeInstance(long instanceId) { + /** + * Remove the instance with instanceId from the manager. + * + * @param instanceId the id of the instance to be removed + * @return the removed instance if the manager contains the instanceId, otherwise null + */ + public Object removeInstanceWithId(long instanceId) { final Object instance = instanceIdsToInstances.get(instanceId); if (instance != null) { instanceIdsToInstances.remove(instanceId); instancesToInstanceIds.remove(instance); } + return instance; } - /** Retrieve the Object paired with instanceId. */ - Object getInstance(long instanceId) { + /** + * Remove the instance from the manager. + * + * @param instance the instance to be removed + * @return the instanceId of the removed instance if the manager contains the value, otherwise + * null + */ + public Long removeInstance(Object instance) { + final Long instanceId = instancesToInstanceIds.get(instance); + if (instanceId != null) { + instanceIdsToInstances.remove(instanceId); + instancesToInstanceIds.remove(instance); + } + return instanceId; + } + + /** + * Retrieve the Object paired with instanceId. + * + * @param instanceId the instanceId of the desired instance + * @return the instance stored with the instanceId if the manager contains the value, otherwise + * null + */ + public Object getInstance(long instanceId) { return instanceIdsToInstances.get(instanceId); } - /** Retrieve the instanceId paired with instance. */ - Long getInstanceId(Object instance) { + /** + * Retrieve the instanceId paired with an instance. + * + * @param instance the value paired with the desired instanceId + * @return the instanceId paired with instance if the manager contains the value, otherwise null + */ + public Long getInstanceId(Object instance) { return instancesToInstanceIds.get(instance); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index 2f987c0f86b3..96ae3ce1fd4d 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -7,6 +7,8 @@ import android.os.Handler; import android.os.Looper; import android.webkit.JavascriptInterface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; @@ -14,13 +16,16 @@ * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets * up. * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. + *

Exposes a single method named `postMessage` to JavaScript, which sends a message to the Dart + * code. + * + *

No messages are sent to Dart after {@link JavaScriptChannel#release} is called. */ -class JavaScriptChannel { +public class JavaScriptChannel implements Releasable { private final MethodChannel methodChannel; - final String javaScriptChannelName; private final Handler platformThreadHandler; + final String javaScriptChannelName; + @Nullable private JavaScriptChannelFlutterApiImpl flutterApi; /** * @param methodChannel the Flutter WebView method channel to which JS messages are sent @@ -35,24 +40,58 @@ class JavaScriptChannel { this.platformThreadHandler = platformThreadHandler; } + /** + * Creates a {@link JavaScriptChannel} that passes arguments of callback methods to Dart. + * + * @param flutterApi the Flutter Api to which JS messages are sent + * @param channelName JavaScript channel the message was sent through + * @param platformThreadHandler handles making callbacks on the desired thread + */ + public JavaScriptChannel( + @NonNull JavaScriptChannelFlutterApiImpl flutterApi, + String channelName, + Handler platformThreadHandler) { + this.flutterApi = flutterApi; + this.javaScriptChannelName = channelName; + this.platformThreadHandler = platformThreadHandler; + methodChannel = null; + } + // Suppressing unused warning as this is invoked from JavaScript. @SuppressWarnings("unused") @JavascriptInterface public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); + Runnable postMessageRunnable = null; + + if (flutterApi != null) { + postMessageRunnable = () -> flutterApi.postMessage(this, message, reply -> {}); + } else if (methodChannel != null) { + postMessageRunnable = + new Runnable() { + @Override + public void run() { + HashMap arguments = new HashMap<>(); + arguments.put("channel", javaScriptChannelName); + arguments.put("message", message); + methodChannel.invokeMethod("javascriptChannelMessage", arguments); + } + }; + } + + if (postMessageRunnable != null) { + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } + } + } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); } + flutterApi = null; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java new file mode 100644 index 000000000000..120f66dbdf9a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; + +/** + * Flutter Api implementation for {@link JavaScriptChannel}. + * + *

Passes arguments of callbacks methods from a {@link JavaScriptChannel} to Dart. + */ +public class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger Handles sending messages to Dart. + * @param instanceManager Maintains instances stored to communicate with Dart objects. + */ + public JavaScriptChannelFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link JavaScriptChannel#postMessage} to Dart. */ + public void postMessage( + JavaScriptChannel javaScriptChannel, String messageArg, Reply callback) { + super.postMessage(instanceManager.getInstanceId(javaScriptChannel), messageArg, callback); + } + + /** + * Communicates to Dart that the reference to a {@link JavaScriptChannel} was removed. + * + * @param javaScriptChannel The instance whose reference will be removed. + * @param callback Reply callback with return value from Dart. + */ + public void dispose(JavaScriptChannel javaScriptChannel, Reply callback) { + final Long instanceId = instanceManager.removeInstance(javaScriptChannel); + if (instanceId != null) { + dispose(instanceId, callback); + } else { + callback.reply(null); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java index 2d42d952955c..ac67a817179e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java @@ -5,44 +5,53 @@ package io.flutter.plugins.webviewflutter; import android.os.Handler; -import android.os.Looper; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; -class JavaScriptChannelHostApiImpl implements GeneratedAndroidWebView.JavaScriptChannelHostApi { +/** + * Host api implementation for {@link JavaScriptChannel}. + * + *

Handles creating {@link JavaScriptChannel}s that intercommunicate with a paired Dart object. + */ +public class JavaScriptChannelHostApiImpl implements JavaScriptChannelHostApi { private final InstanceManager instanceManager; private final JavaScriptChannelCreator javaScriptChannelCreator; - private final JavaScriptChannelFlutterApi javaScriptChannelFlutterApi; + private final JavaScriptChannelFlutterApiImpl flutterApi; private final Handler platformThreadHandler; - static class JavaScriptChannelCreator { - JavaScriptChannel createJavaScriptChannel( - Long instanceId, - JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + /** Handles creating {@link JavaScriptChannel}s for a {@link JavaScriptChannelHostApiImpl}. */ + public static class JavaScriptChannelCreator { + /** + * Creates a {@link JavaScriptChannel}. + * + * @param flutterApi handles sending messages to Dart + * @param channelName JavaScript channel the message should be sent through + * @param platformThreadHandler handles making callbacks on the desired thread + * @return the created {@link JavaScriptChannel} + */ + public JavaScriptChannel createJavaScriptChannel( + JavaScriptChannelFlutterApiImpl flutterApi, String channelName, Handler platformThreadHandler) { - return new JavaScriptChannel(null, channelName, platformThreadHandler) { - @Override - public void postMessage(String message) { - final Runnable postMessageRunnable = - () -> javaScriptChannelFlutterApi.postMessage(instanceId, message, reply -> {}); - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } - }; + return new JavaScriptChannel(flutterApi, channelName, platformThreadHandler); } } - JavaScriptChannelHostApiImpl( + /** + * Creates a host API that handles creating {@link JavaScriptChannel}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param javaScriptChannelCreator handles creating {@link JavaScriptChannel}s + * @param flutterApi handles sending messages to Dart + * @param platformThreadHandler handles making callbacks on the desired thread + */ + public JavaScriptChannelHostApiImpl( InstanceManager instanceManager, JavaScriptChannelCreator javaScriptChannelCreator, - JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + JavaScriptChannelFlutterApiImpl flutterApi, Handler platformThreadHandler) { this.instanceManager = instanceManager; this.javaScriptChannelCreator = javaScriptChannelCreator; - this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi; + this.flutterApi = flutterApi; this.platformThreadHandler = platformThreadHandler; } @@ -50,12 +59,7 @@ public void postMessage(String message) { public void create(Long instanceId, String channelName) { final JavaScriptChannel javaScriptChannel = javaScriptChannelCreator.createJavaScriptChannel( - instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler); + flutterApi, channelName, platformThreadHandler); instanceManager.addInstance(javaScriptChannel, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java new file mode 100644 index 000000000000..9c4ed7650640 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.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.webviewflutter; + +/** + * Represents a resource, or a holder of resources, which may be released once they are no longer + * needed. + */ +interface Releasable { + /** Notify that that the reference to an object will be removed by a holder. */ + void release(); +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java new file mode 100644 index 000000000000..2ab9275b41c3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; + +/** + * Flutter Api implementation for {@link WebChromeClient}. + * + *

Passes arguments of callbacks methods from a {@link WebChromeClient} to Dart. + */ +public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public WebChromeClientFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link WebChromeClient#onProgressChanged} to Dart. */ + public void onProgressChanged( + WebChromeClient webChromeClient, WebView webView, Long progress, Reply callback) { + super.onProgressChanged( + instanceManager.getInstanceId(webChromeClient), + instanceManager.getInstanceId(webView), + progress, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link WebChromeClient}} was removed. + * + * @param webChromeClient the instance whose reference will be removed + * @param callback reply callback with return value from Dart + */ + public void dispose(WebChromeClient webChromeClient, Reply callback) { + final Long instanceId = instanceManager.removeInstance(webChromeClient); + if (instanceId != null) { + dispose(instanceId, callback); + } else { + callback.reply(null); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 32f8fcbdeed9..f8bc5129a3ca 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -11,68 +11,125 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; -class WebChromeClientHostApiImpl implements GeneratedAndroidWebView.WebChromeClientHostApi { +/** + * Host api implementation for {@link WebChromeClient}. + * + *

Handles creating {@link WebChromeClient}s that intercommunicate with a paired Dart object. + */ +public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { private final InstanceManager instanceManager; private final WebChromeClientCreator webChromeClientCreator; - private final WebChromeClientFlutterApi webChromeClientFlutterApi; - - static class WebChromeClientCreator { - WebChromeClient createWebChromeClient( - Long instanceId, - InstanceManager instanceManager, - WebViewClient webViewClient, - WebChromeClientFlutterApi webChromeClientFlutterApi) { - return new WebChromeClient() { - // Verifies that a url opened by `Window.open` has a secure url. - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient newWindowWebViewClient = - new WebViewClient() { - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - webViewClient.shouldOverrideUrlLoading(view, request); - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClient.shouldOverrideUrlLoading(view, url); - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(newWindowWebViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - webChromeClientFlutterApi.onProgressChanged( - instanceId, instanceManager.getInstanceId(view), (long) progress, reply -> {}); - } - }; + private final WebChromeClientFlutterApiImpl flutterApi; + + /** + * Implementation of {@link WebChromeClient} that passes arguments of callback methods to Dart. + */ + public static class WebChromeClientImpl extends WebChromeClient implements Releasable { + @Nullable private WebChromeClientFlutterApiImpl flutterApi; + private WebViewClient webViewClient; + + /** + * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} + */ + public WebChromeClientImpl( + @NonNull WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + this.flutterApi = flutterApi; + this.webViewClient = webViewClient; + } + + // Verifies that a url opened by `Window.open` has a secure url. + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient newWindowWebViewClient = + new WebViewClient() { + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webViewClient.shouldOverrideUrlLoading(view, request); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webViewClient.shouldOverrideUrlLoading(view, url); + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(newWindowWebViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + if (flutterApi != null) { + flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); + } + } + + /** + * Set the {@link WebViewClient} that calls to {@link WebChromeClient#onCreateWindow} are passed + * to. + * + * @param webViewClient the forwarding {@link WebViewClient} + */ + public void setWebViewClient(WebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } + } + + /** Handles creating {@link WebChromeClient}s for a {@link WebChromeClientHostApiImpl}. */ + public static class WebChromeClientCreator { + /** + * Creates a {@link DownloadListenerHostApiImpl.DownloadListenerImpl}. + * + * @param flutterApi handles sending messages to Dart + * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} + * @return the created {@link DownloadListenerHostApiImpl.DownloadListenerImpl} + */ + public WebChromeClientImpl createWebChromeClient( + WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + return new WebChromeClientImpl(flutterApi, webViewClient); } } - WebChromeClientHostApiImpl( + /** + * Creates a host API that handles creating {@link WebChromeClient}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webChromeClientCreator handles creating {@link WebChromeClient}s + * @param flutterApi handles sending messages to Dart + */ + public WebChromeClientHostApiImpl( InstanceManager instanceManager, WebChromeClientCreator webChromeClientCreator, - WebChromeClientFlutterApi webChromeClientFlutterApi) { + WebChromeClientFlutterApiImpl flutterApi) { this.instanceManager = instanceManager; this.webChromeClientCreator = webChromeClientCreator; - this.webChromeClientFlutterApi = webChromeClientFlutterApi; + this.flutterApi = flutterApi; } @Override @@ -80,13 +137,7 @@ public void create(Long instanceId, Long webViewClientInstanceId) { final WebViewClient webViewClient = (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); final WebChromeClient webChromeClient = - webChromeClientCreator.createWebChromeClient( - instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); + webChromeClientCreator.createWebChromeClient(flutterApi, webViewClient); instanceManager.addInstance(webChromeClient, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java index e70a867c23ff..239ef473b546 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -6,18 +6,38 @@ import android.webkit.WebSettings; import android.webkit.WebView; - -class WebSettingsHostApiImpl implements GeneratedAndroidWebView.WebSettingsHostApi { +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; + +/** + * Host api implementation for {@link WebSettings}. + * + *

Handles creating {@link WebSettings}s that intercommunicate with a paired Dart object. + */ +public class WebSettingsHostApiImpl implements WebSettingsHostApi { private final InstanceManager instanceManager; private final WebSettingsCreator webSettingsCreator; - static class WebSettingsCreator { - WebSettings createWebSettings(WebView webView) { + /** Handles creating {@link WebSettings} for a {@link WebSettingsHostApiImpl}. */ + public static class WebSettingsCreator { + /** + * Creates a {@link WebSettings}. + * + * @param webView the {@link WebView} which the settings affect + * @return the created {@link WebSettings} + */ + public WebSettings createWebSettings(WebView webView) { return webView.getSettings(); } } - WebSettingsHostApiImpl(InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) { + /** + * Creates a host API that handles creating {@link WebSettings} and invoke its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webSettingsCreator handles creating {@link WebSettings}s + */ + public WebSettingsHostApiImpl( + InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) { this.instanceManager = instanceManager; this.webSettingsCreator = webSettingsCreator; } @@ -30,7 +50,7 @@ public void create(Long instanceId, Long webViewInstanceId) { @Override public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); + instanceManager.removeInstanceWithId(instanceId); } @Override 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 new file mode 100644 index 000000000000..9e462faa58a7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -0,0 +1,198 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; + +/** + * Flutter Api implementation for {@link WebViewClient}. + * + *

Passes arguments of callbacks methods from a {@link WebViewClient} to Dart. + */ +public class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { + private final InstanceManager instanceManager; + + @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; + } + + @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; + } + + @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()); + 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; + } + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public WebViewClientFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link WebViewClient#onPageStarted} to Dart. */ + public void onPageStarted( + WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { + onPageStarted( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + urlArg, + callback); + } + + /** Passes arguments from {@link WebViewClient#onPageFinished} to Dart. */ + public void onPageFinished( + WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { + onPageFinished( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + urlArg, + callback); + } + + /** + * Passes arguments from {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, + * WebResourceError)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.M) + public void onReceivedRequestError( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + WebResourceError error, + Reply callback) { + onReceivedRequestError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + callback); + } + + /** + * Passes arguments from {@link androidx.webkit.WebViewClientCompat#onReceivedError(WebView, + * WebResourceRequest, WebResourceError)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onReceivedRequestError( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + WebResourceErrorCompat error, + Reply callback) { + onReceivedRequestError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + callback); + } + + /** + * Passes arguments from {@link WebViewClient#onReceivedError(WebView, int, String, String)} to + * Dart. + */ + public void onReceivedError( + WebViewClient webViewClient, + WebView webView, + Long errorCodeArg, + String descriptionArg, + String failingUrlArg, + Reply callback) { + onReceivedError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + errorCodeArg, + descriptionArg, + failingUrlArg, + callback); + } + + /** + * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView, + * WebResourceRequest)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void requestLoading( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + Reply callback) { + requestLoading( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + callback); + } + + /** + * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView, String)} to Dart. + */ + public void urlLoading( + WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { + urlLoading( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + urlArg, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link WebViewClient} was removed. + * + * @param webViewClient the instance whose reference will be removed + * @param callback reply callback with return value from Dart + */ + public void dispose(WebViewClient webViewClient, Reply callback) { + final Long instanceId = instanceManager.removeInstance(webViewClient); + if (instanceId != null) { + dispose(instanceId, callback); + } else { + callback.reply(null); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java index 4d17eb129db8..6b659fae9c0f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -14,61 +14,200 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; -class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi { +/** + * Host api implementation for {@link WebViewClient}. + * + *

Handles creating {@link WebViewClient}s that intercommunicate with a paired Dart object. + */ +public class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi { private final InstanceManager instanceManager; private final WebViewClientCreator webViewClientCreator; - private final WebViewClientFlutterApi webViewClientFlutterApi; + private final WebViewClientFlutterApiImpl flutterApi; + + /** + * An interface implemented by a class that extends {@link WebViewClient} and {@link Releasable}. + */ + public interface ReleasableWebViewClient extends Releasable {} + + /** Implementation of {@link WebViewClient} that passes arguments of callback methods to Dart. */ + @RequiresApi(Build.VERSION_CODES.N) + public static class WebViewClientImpl extends WebViewClient implements ReleasableWebViewClient { + @Nullable private WebViewClientFlutterApiImpl flutterApi; + private final boolean shouldOverrideUrlLoading; + + /** + * Creates a {@link WebViewClient} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + * @param shouldOverrideUrlLoading whether loading a url should be overridden + */ + public WebViewClientImpl( + @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } - @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()); + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (flutterApi != null) { + flutterApi.onPageStarted(this, view, url, reply -> {}); + } + } - return errorData; - } + @Override + public void onPageFinished(WebView view, String url) { + if (flutterApi != null) { + flutterApi.onPageFinished(this, view, url, reply -> {}); + } + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (flutterApi != null) { + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (flutterApi != null) { + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); + } + } - @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()); + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (flutterApi != null) { + flutterApi.requestLoading(this, view, request, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (flutterApi != null) { + flutterApi.urlLoading(this, view, url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } - return errorData; + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } } - @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()); - 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; + /** + * Implementation of {@link WebViewClientCompat} that passes arguments of callback methods to + * Dart. + */ + public static class WebViewClientCompatImpl extends WebViewClientCompat + implements ReleasableWebViewClient { + private @Nullable WebViewClientFlutterApiImpl flutterApi; + private final boolean shouldOverrideUrlLoading; + + public WebViewClientCompatImpl( + @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (flutterApi != null) { + flutterApi.onPageStarted(this, view, url, reply -> {}); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + if (flutterApi != null) { + flutterApi.onPageFinished(this, view, url, reply -> {}); + } + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (flutterApi != null) { + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (flutterApi != null) { + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + if (flutterApi != null) { + flutterApi.requestLoading(this, view, request, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (flutterApi != null) { + flutterApi.urlLoading(this, view, url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } } - static class WebViewClientCreator { - WebViewClient createWebViewClient( - Long instanceId, - InstanceManager instanceManager, - Boolean shouldOverrideUrlLoading, - WebViewClientFlutterApi webViewClientFlutterApi) { + /** Handles creating {@link WebViewClient}s for a {@link WebViewClientHostApiImpl}. */ + public static class WebViewClientCreator { + /** + * Creates a {@link WebViewClient}. + * + * @param flutterApi handles sending messages to Dart + * @return the created {@link WebViewClient} + */ + public WebViewClient createWebViewClient( + WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { // WebViewClientCompat is used to get // shouldOverrideUrlLoading(WebView view, WebResourceRequest request) // invoked by the webview on older Android devices, without it pages that use iframes will @@ -78,162 +217,33 @@ WebViewClient createWebViewClient( // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see // https://github.com/flutter/flutter/issues/29446. if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return new WebViewClient() { - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewClientFlutterApi.onPageStarted( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onPageFinished(WebView view, String url) { - webViewClientFlutterApi.onPageFinished( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - webViewClientFlutterApi.onReceivedRequestError( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - createWebResourceErrorData(error), - reply -> {}); - } - - @SuppressWarnings("deprecation") - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - webViewClientFlutterApi.onReceivedError( - instanceId, - instanceManager.getInstanceId(view), - (long) errorCode, - description, - failingUrl, - reply -> {}); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - webViewClientFlutterApi.requestLoading( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - reply -> {}); - return shouldOverrideUrlLoading; - } - - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClientFlutterApi.urlLoading( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - return shouldOverrideUrlLoading; - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; + return new WebViewClientImpl(flutterApi, shouldOverrideUrlLoading); } else { - return new WebViewClientCompat() { - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewClientFlutterApi.onPageStarted( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onPageFinished(WebView view, String url) { - webViewClientFlutterApi.onPageFinished( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - webViewClientFlutterApi.onReceivedRequestError( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - createWebResourceErrorData(error), - reply -> {}); - } - - @SuppressWarnings("deprecation") - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - webViewClientFlutterApi.onReceivedError( - instanceId, - instanceManager.getInstanceId(view), - (long) errorCode, - description, - failingUrl, - reply -> {}); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - webViewClientFlutterApi.requestLoading( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - reply -> {}); - return shouldOverrideUrlLoading; - } - - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClientFlutterApi.urlLoading( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - return shouldOverrideUrlLoading; - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; + return new WebViewClientCompatImpl(flutterApi, shouldOverrideUrlLoading); } } } - WebViewClientHostApiImpl( + /** + * Creates a host API that handles creating {@link WebViewClient}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webViewClientCreator handles creating {@link WebViewClient}s + * @param flutterApi handles sending messages to Dart + */ + public WebViewClientHostApiImpl( InstanceManager instanceManager, WebViewClientCreator webViewClientCreator, - WebViewClientFlutterApi webViewClientFlutterApi) { + WebViewClientFlutterApiImpl flutterApi) { this.instanceManager = instanceManager; this.webViewClientCreator = webViewClientCreator; - this.webViewClientFlutterApi = webViewClientFlutterApi; + this.flutterApi = flutterApi; } @Override public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { final WebViewClient webViewClient = - webViewClientCreator.createWebViewClient( - instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); + webViewClientCreator.createWebViewClient(flutterApi, shouldOverrideUrlLoading); instanceManager.addInstance(webViewClient, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } 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 35bdc608d6ff..2d96fc3f8a60 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 @@ -4,36 +4,118 @@ package io.flutter.plugins.webviewflutter; +import android.annotation.SuppressLint; import android.content.Context; +import android.hardware.display.DisplayManager; import android.view.View; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.ReleasableWebViewClient; +import java.util.HashMap; import java.util.Map; -class WebViewHostApiImpl implements GeneratedAndroidWebView.WebViewHostApi { +/** + * Host api implementation for {@link WebView}. + * + *

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; private final Context context; - - static class WebViewProxy { - WebView createWebView(Context context) { + // Only used with WebView using virtual displays. + @Nullable private final View containerView; + + /** Handles creating and calling static methods for {@link WebView}s. */ + public static class WebViewProxy { + /** + * Creates a {@link WebViewPlatformView}. + * + * @param context an Activity Context to access application assets + * @return the created {@link WebViewPlatformView} + */ + public WebViewPlatformView createWebView(Context context) { return new WebViewPlatformView(context); } - WebView createInputAwareWebView(Context context) { - return new InputAwareWebViewPlatformView(context, null); + /** + * Creates a {@link InputAwareWebViewPlatformView}. + * + * @param context an Activity Context to access application assets + * @param containerView parent View of the WebView + * @return the created {@link InputAwareWebViewPlatformView} + */ + public InputAwareWebViewPlatformView createInputAwareWebView( + Context context, @Nullable View containerView) { + return new InputAwareWebViewPlatformView(context, containerView); } - void setWebContentsDebuggingEnabled(boolean enabled) { + /** + * Forwards call to {@link WebView#setWebContentsDebuggingEnabled}. + * + * @param enabled whether debugging should be enabled + */ + public void setWebContentsDebuggingEnabled(boolean enabled) { WebView.setWebContentsDebuggingEnabled(enabled); } } - private static class WebViewPlatformView extends WebView implements PlatformView { + private static class ReleasableValue { + @Nullable private T value; + + ReleasableValue() {} + + ReleasableValue(@Nullable T value) { + this.value = value; + } + + void set(@Nullable T newValue) { + release(); + value = newValue; + } + + @Nullable + T get() { + return value; + } + + void release() { + if (value != null) { + value.release(); + } + value = null; + } + } + + /** Implementation of {@link WebView} that can be used as a Flutter {@link PlatformView}s. */ + public static class WebViewPlatformView extends WebView implements PlatformView, Releasable { + private final ReleasableValue + currentWebViewClient = new ReleasableValue<>(); + private final ReleasableValue currentDownloadListener = + new ReleasableValue<>(); + private final ReleasableValue currentWebChromeClient = + new ReleasableValue<>(); + private final Map> javaScriptInterfaces = + new HashMap<>(); + + /** + * Creates a {@link WebViewPlatformView}. + * + * @param context an Activity Context to access application assets. This value cannot be null. + */ public WebViewPlatformView(Context context) { super(context); } @@ -47,11 +129,85 @@ public View getView() { public void dispose() { destroy(); } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + currentWebViewClient.set((ReleasableWebViewClient) webViewClient); + + final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); + if (webChromeClient != null) { + ((WebChromeClientImpl) webChromeClient).setWebViewClient(webViewClient); + } + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + currentDownloadListener.set((DownloadListenerImpl) listener); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + currentWebChromeClient.set((WebChromeClientImpl) client); + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null && javaScriptChannel.get() != object) { + javaScriptChannel.release(); + } + javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + javaScriptChannel.release(); + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + currentWebViewClient.release(); + currentDownloadListener.release(); + currentWebChromeClient.release(); + for (ReleasableValue channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } } - private static class InputAwareWebViewPlatformView extends InputAwareWebView - implements PlatformView { - InputAwareWebViewPlatformView(Context context, View containerView) { + /** + * Implementation of {@link InputAwareWebView} that can be used as a Flutter {@link + * PlatformView}s. + */ + @SuppressLint("ViewConstructor") + public static class InputAwareWebViewPlatformView extends InputAwareWebView + implements PlatformView, Releasable { + private final ReleasableValue + currentWebViewClient = new ReleasableValue<>(); + private final ReleasableValue currentDownloadListener = + new ReleasableValue<>(); + private final ReleasableValue currentWebChromeClient = + new ReleasableValue<>(); + private final Map> javaScriptInterfaces = + new HashMap<>(); + + /** + * Creates a {@link InputAwareWebViewPlatformView}. + * + * @param context an Activity Context to access application assets. This value cannot be null. + */ + public InputAwareWebViewPlatformView(Context context, View containerView) { super(context, containerView); } @@ -72,7 +228,7 @@ public void onFlutterViewDetached() { @Override public void dispose() { - dispose(); + super.dispose(); destroy(); } @@ -85,26 +241,104 @@ public void onInputConnectionLocked() { public void onInputConnectionUnlocked() { unlockInputConnection(); } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + currentWebViewClient.set((ReleasableWebViewClient) webViewClient); + + final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); + if (webChromeClient != null) { + webChromeClient.setWebViewClient(webViewClient); + } + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + currentDownloadListener.set((DownloadListenerImpl) listener); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + currentWebChromeClient.set((WebChromeClientImpl) client); + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null && javaScriptChannel.get() != object) { + javaScriptChannel.release(); + } + javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + javaScriptChannel.release(); + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + currentWebViewClient.release(); + currentDownloadListener.release(); + currentWebChromeClient.release(); + for (ReleasableValue channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } } - WebViewHostApiImpl(InstanceManager instanceManager, WebViewProxy webViewProxy, Context context) { + /** + * Creates a host API that handles creating {@link WebView}s and invoking its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webViewProxy handles creating {@link WebView}s and calling its static methods + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView parent of the webView + */ + public WebViewHostApiImpl( + InstanceManager instanceManager, + WebViewProxy webViewProxy, + Context context, + @Nullable View containerView) { this.instanceManager = instanceManager; this.webViewProxy = webViewProxy; this.context = context; + this.containerView = containerView; } @Override public void create(Long instanceId, Boolean useHybridComposition) { + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + final WebView webView = useHybridComposition ? webViewProxy.createWebView(context) - : webViewProxy.createInputAwareWebView(context); + : webViewProxy.createInputAwareWebView(context, containerView); + + displayListenerProxy.onPostWebViewInitialization(displayManager); instanceManager.addInstance(webView, instanceId); } @Override public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); + final WebView instance = (WebView) instanceManager.removeInstanceWithId(instanceId); + if (instance != null) { + ((Releasable) instance).release(); + } } @Override @@ -116,7 +350,8 @@ public void loadUrl(Long instanceId, String url, Map headers) { @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - return webView.getUrl(); + final String result = webView.getUrl(); + return result != null ? result : nullStringIdentifier; } @Override @@ -165,7 +400,8 @@ public void evaluateJavascript( @Override public String getTitle(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - return webView.getTitle(); + final String result = webView.getTitle(); + return result != null ? result : nullStringIdentifier; } @Override @@ -228,6 +464,6 @@ public void setDownloadListener(Long instanceId, Long listenerInstanceId) { @Override public void setWebChromeClient(Long instanceId, Long clientInstanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(instanceId)); + webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId)); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java index 5ba073c7f418..239119375e83 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java @@ -6,11 +6,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.webkit.DownloadListener; import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -21,45 +23,49 @@ public class DownloadListenerTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock public DownloadListenerFlutterApi mockFlutterApi; + @Mock public DownloadListenerFlutterApiImpl mockFlutterApi; - InstanceManager testInstanceManager; - DownloadListenerHostApiImpl testHostApiImpl; - DownloadListener testDownloadListener; + InstanceManager instanceManager; + DownloadListenerHostApiImpl hostApiImpl; + DownloadListenerImpl downloadListener; @Before public void setUp() { - testInstanceManager = new InstanceManager(); + instanceManager = new InstanceManager(); final DownloadListenerCreator downloadListenerCreator = new DownloadListenerCreator() { @Override - DownloadListener createDownloadListener( - Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { - testDownloadListener = - super.createDownloadListener(instanceId, downloadListenerFlutterApi); - return testDownloadListener; + public DownloadListenerImpl createDownloadListener( + DownloadListenerFlutterApiImpl flutterApi) { + downloadListener = super.createDownloadListener(flutterApi); + return downloadListener; } }; - testHostApiImpl = - new DownloadListenerHostApiImpl( - testInstanceManager, downloadListenerCreator, mockFlutterApi); - testHostApiImpl.create(0L); + hostApiImpl = + new DownloadListenerHostApiImpl(instanceManager, downloadListenerCreator, mockFlutterApi); + hostApiImpl.create(0L); } @Test public void postMessage() { - testDownloadListener.onDownloadStart( + downloadListener.onDownloadStart( "https://www.google.com", "userAgent", "contentDisposition", "mimetype", 54); verify(mockFlutterApi) .onDownloadStart( - eq(0L), + eq(downloadListener), eq("https://www.google.com"), eq("userAgent"), eq("contentDisposition"), eq("mimetype"), eq(54L), any()); + + reset(mockFlutterApi); + downloadListener.release(); + downloadListener.onDownloadStart("", "", "", "", 23); + verify(mockFlutterApi, never()) + .onDownloadStart((DownloadListener) any(), any(), any(), any(), any(), eq(23), any()); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java index 697ea0b70b90..3de81da81bec 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java @@ -6,10 +6,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.os.Handler; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; import io.flutter.plugins.webviewflutter.JavaScriptChannelHostApiImpl.JavaScriptChannelCreator; import org.junit.Before; import org.junit.Rule; @@ -21,40 +22,44 @@ public class JavaScriptChannelTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock public GeneratedAndroidWebView.JavaScriptChannelFlutterApi mockFlutterApi; + @Mock public JavaScriptChannelFlutterApiImpl mockFlutterApi; - InstanceManager testInstanceManager; - JavaScriptChannelHostApiImpl testHostApiImpl; - JavaScriptChannel testJavaScriptChannel; + InstanceManager instanceManager; + JavaScriptChannelHostApiImpl hostApiImpl; + JavaScriptChannel javaScriptChannel; @Before public void setUp() { - testInstanceManager = new InstanceManager(); + instanceManager = new InstanceManager(); final JavaScriptChannelCreator javaScriptChannelCreator = new JavaScriptChannelCreator() { @Override - JavaScriptChannel createJavaScriptChannel( - Long instanceId, - JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + public JavaScriptChannel createJavaScriptChannel( + JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi, String channelName, Handler platformThreadHandler) { - testJavaScriptChannel = + javaScriptChannel = super.createJavaScriptChannel( - instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler); - return testJavaScriptChannel; + javaScriptChannelFlutterApi, channelName, platformThreadHandler); + return javaScriptChannel; } }; - testHostApiImpl = + hostApiImpl = new JavaScriptChannelHostApiImpl( - testInstanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler()); - testHostApiImpl.create(0L, "aChannelName"); + instanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler()); + hostApiImpl.create(0L, "aChannelName"); } @Test public void postMessage() { - testJavaScriptChannel.postMessage("A message post."); - verify(mockFlutterApi).postMessage(eq(0L), eq("A message post."), any()); + javaScriptChannel.postMessage("A message post."); + verify(mockFlutterApi).postMessage(eq(javaScriptChannel), eq("A message post."), any()); + + reset(mockFlutterApi); + javaScriptChannel.release(); + javaScriptChannel.postMessage("a message"); + verify(mockFlutterApi, never()).postMessage((JavaScriptChannel) any(), any(), any()); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 5ab3ab10fe03..d6593ab24462 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -6,13 +6,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -23,45 +25,45 @@ public class WebChromeClientTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock public WebChromeClientFlutterApi mockFlutterApi; + @Mock public WebChromeClientFlutterApiImpl mockFlutterApi; @Mock public WebView mockWebView; @Mock public WebViewClient mockWebViewClient; - InstanceManager testInstanceManager; - WebChromeClientHostApiImpl testHostApiImpl; - WebChromeClient testWebChromeClient; + InstanceManager instanceManager; + WebChromeClientHostApiImpl hostApiImpl; + WebChromeClientImpl webChromeClient; @Before public void setUp() { - testInstanceManager = new InstanceManager(); - testInstanceManager.addInstance(mockWebView, 0L); - testInstanceManager.addInstance(mockWebViewClient, 1L); + instanceManager = new InstanceManager(); + instanceManager.addInstance(mockWebView, 0L); + instanceManager.addInstance(mockWebViewClient, 1L); final WebChromeClientCreator webChromeClientCreator = new WebChromeClientCreator() { @Override - WebChromeClient createWebChromeClient( - Long instanceId, - InstanceManager instanceManager, - WebViewClient webViewClient, - WebChromeClientFlutterApi webChromeClientFlutterApi) { - testWebChromeClient = - super.createWebChromeClient( - instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); - return testWebChromeClient; + public WebChromeClientImpl createWebChromeClient( + WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + webChromeClient = super.createWebChromeClient(flutterApi, webViewClient); + return webChromeClient; } }; - testHostApiImpl = - new WebChromeClientHostApiImpl(testInstanceManager, webChromeClientCreator, mockFlutterApi); - testHostApiImpl.create(2L, 1L); + hostApiImpl = + new WebChromeClientHostApiImpl(instanceManager, webChromeClientCreator, mockFlutterApi); + hostApiImpl.create(2L, 1L); } @Test public void onProgressChanged() { - testWebChromeClient.onProgressChanged(mockWebView, 23); - verify(mockFlutterApi).onProgressChanged(eq(2L), eq(0L), eq(23L), any()); + webChromeClient.onProgressChanged(mockWebView, 23); + verify(mockFlutterApi).onProgressChanged(eq(webChromeClient), eq(mockWebView), eq(23L), any()); + + reset(mockFlutterApi); + webChromeClient.release(); + webChromeClient.onProgressChanged(mockWebView, 11); + verify(mockFlutterApi, never()).onProgressChanged((WebChromeClient) any(), any(), any(), any()); } } 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 f6d2054564e2..62d272366a6f 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 @@ -6,11 +6,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.webkit.WebView; import android.webkit.WebViewClient; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCompatImpl; import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCreator; import org.junit.Before; import org.junit.Rule; @@ -22,56 +24,76 @@ public class WebViewClientTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock public WebViewClientFlutterApi mockFlutterApi; + @Mock public WebViewClientFlutterApiImpl mockFlutterApi; @Mock public WebView mockWebView; - InstanceManager testInstanceManager; - WebViewClientHostApiImpl testHostApiImpl; - WebViewClient testWebViewClient; + InstanceManager instanceManager; + WebViewClientHostApiImpl hostApiImpl; + WebViewClientCompatImpl webViewClient; @Before public void setUp() { - testInstanceManager = new InstanceManager(); - testInstanceManager.addInstance(mockWebView, 0L); + instanceManager = new InstanceManager(); + instanceManager.addInstance(mockWebView, 0L); final WebViewClientCreator webViewClientCreator = new WebViewClientCreator() { @Override - WebViewClient createWebViewClient( - Long instanceId, - InstanceManager instanceManager, - Boolean shouldOverrideUrlLoading, - WebViewClientFlutterApi webViewClientFlutterApi) { - testWebViewClient = - super.createWebViewClient( - instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); - return testWebViewClient; + public WebViewClient createWebViewClient( + WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + webViewClient = + (WebViewClientCompatImpl) + super.createWebViewClient(flutterApi, shouldOverrideUrlLoading); + return webViewClient; } }; - testHostApiImpl = - new WebViewClientHostApiImpl(testInstanceManager, webViewClientCreator, mockFlutterApi); - testHostApiImpl.create(1L, true); + hostApiImpl = + new WebViewClientHostApiImpl(instanceManager, webViewClientCreator, mockFlutterApi); + hostApiImpl.create(1L, true); } @Test public void onPageStarted() { - testWebViewClient.onPageStarted(mockWebView, "https://www.google.com", null); - verify(mockFlutterApi).onPageStarted(eq(1L), eq(0L), eq("https://www.google.com"), any()); + webViewClient.onPageStarted(mockWebView, "https://www.google.com", null); + verify(mockFlutterApi) + .onPageStarted(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); + + reset(mockFlutterApi); + webViewClient.release(); + webViewClient.onPageStarted(mockWebView, "", null); + verify(mockFlutterApi, never()).onPageStarted((WebViewClient) any(), any(), any(), any()); } @Test public void onReceivedError() { - testWebViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com"); + webViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com"); verify(mockFlutterApi) .onReceivedError( - eq(1L), eq(0L), eq(32L), eq("description"), eq("https://www.google.com"), any()); + eq(webViewClient), + eq(mockWebView), + eq(32L), + eq("description"), + eq("https://www.google.com"), + any()); + + reset(mockFlutterApi); + webViewClient.release(); + webViewClient.onReceivedError(mockWebView, 33, "", ""); + verify(mockFlutterApi, never()) + .onReceivedError((WebViewClient) any(), any(), any(), any(), any(), any()); } @Test public void urlLoading() { - testWebViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com"); - verify(mockFlutterApi).urlLoading(eq(1L), eq(0L), eq("https://www.google.com"), any()); + webViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com"); + verify(mockFlutterApi) + .urlLoading(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); + + reset(mockFlutterApi); + webViewClient.release(); + webViewClient.shouldOverrideUrlLoading(mockWebView, ""); + verify(mockFlutterApi, never()).urlLoading((WebViewClient) any(), any(), any(), any()); } } 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 b914ce913e76..e506188d0c9d 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 @@ -13,8 +13,13 @@ import android.content.Context; import android.webkit.DownloadListener; import android.webkit.ValueCallback; -import android.webkit.WebView; +import android.webkit.WebChromeClient; import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientImpl; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView; import java.util.HashMap; import org.junit.Before; import org.junit.Rule; @@ -27,7 +32,7 @@ public class WebViewTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock public WebView mockWebView; + @Mock public WebViewPlatformView mockWebView; @Mock WebViewHostApiImpl.WebViewProxy mockWebViewProxy; @@ -40,45 +45,115 @@ public class WebViewTest { public void setUp() { testInstanceManager = new InstanceManager(); when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView); - testHostApiImpl = new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext); + testHostApiImpl = + new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext, null); testHostApiImpl.create(0L, true); } @Test - public void errorCodes() { - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), - "authentication"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), - "failedSslHandshake"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), - "proxyAuthentication"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), - "tooManyRequests"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), - "unsafeResource"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), - "unsupportedAuthScheme"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), - "unsupportedScheme"); + public void releaseWebView() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseWebViewDependents() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + // Release should be called on the object added above. + webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); + verify(mockJavaScriptChannel).release(); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel2).release(); + } + + @Test + public void releaseInputAwareWebView() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseInputAwareWebViewDependents() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + // Release should be called on the object added above. + webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); + verify(mockJavaScriptChannel).release(); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel2).release(); } @Test @@ -195,7 +270,8 @@ public void setWebViewClient() { @Test public void addJavaScriptChannel() { - final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); testInstanceManager.addInstance(javaScriptChannel, 1L); testHostApiImpl.addJavaScriptChannel(0L, 1L); @@ -204,7 +280,8 @@ public void addJavaScriptChannel() { @Test public void removeJavaScriptChannel() { - final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); testInstanceManager.addInstance(javaScriptChannel, 1L); testHostApiImpl.removeJavaScriptChannel(0L, 1L); @@ -219,4 +296,13 @@ public void setDownloadListener() { testHostApiImpl.setDownloadListener(0L, 1L); verify(mockWebView).setDownloadListener(mockDownloadListener); } + + @Test + public void setWebChromeClient() { + final WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + testInstanceManager.addInstance(mockWebChromeClient, 1L); + + testHostApiImpl.setWebChromeClient(0L, 1L); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + } } From d22be768e9d5f48d61aca7ff93db8d6a6ded28b1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 10 Nov 2021 19:28:02 -0800 Subject: [PATCH 156/259] [google_maps_flutter] Add new XCUITests that do not require permissions. (#4496) --- .../ios/RunnerUITests/GoogleMapsUITests.m | 57 +++++++++++++++++++ .../example/lib/map_click.dart | 18 +++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m index 9a8ded716ec6..3aaf7d3d1f89 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m @@ -17,6 +17,8 @@ - (void)setUp { self.app = [[XCUIApplication alloc] init]; [self.app launch]; + // The location permission interception is currently not working. + // See: https://github.com/flutter/flutter/issues/93325. [self addUIInterruptionMonitorWithDescription:@"Permission popups" handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { @@ -64,4 +66,59 @@ - (void)skip_testUserInterface { [compass tap]; } +- (void)testMapCoordinatesPage { + XCUIApplication* app = self.app; + XCUIElement* mapCoordinates = app.staticTexts[@"Map coordinates"]; + if (![mapCoordinates waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Map coordinates''"); + } + [mapCoordinates tap]; + + XCUIElement* platformView = app.otherElements[@"platform_view[0]"]; + if (![platformView waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find platform view"); + } + + XCUIElement* getVisibleRegionBoundsButton = app.buttons[@"Get Visible Region Bounds"]; + if (![getVisibleRegionBoundsButton waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Get Visible Region Bounds''"); + } + [getVisibleRegionBoundsButton tap]; +} + +- (void)testMapClickPage { + XCUIApplication* app = self.app; + XCUIElement* mapClick = app.staticTexts[@"Map click"]; + if (![mapClick waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Map click''"); + } + [mapClick tap]; + + XCUIElement* platformView = app.otherElements[@"platform_view[0]"]; + if (![platformView waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find platform view"); + } + + [platformView tap]; + + XCUIElement* tapped = app.staticTexts[@"Tapped"]; + if (![tapped waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'tapped''"); + } + + [platformView pressForDuration:5.0]; + + XCUIElement* longPressed = app.staticTexts[@"Long pressed"]; + if (![longPressed waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'longPressed''"); + } +} + @end 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 a46fc5fba420..e44f3d9d6a2e 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 @@ -68,15 +68,27 @@ class _MapClickBodyState extends State<_MapClickBody> { if (mapController != null) { final String lastTap = 'Tap:\n${_lastTap ?? ""}\n'; final String lastLongPress = 'Long press:\n${_lastLongPress ?? ""}'; - columnChildren - .add(Center(child: Text(lastTap, textAlign: TextAlign.center))); + columnChildren.add(Center( + child: Text( + lastTap, + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + _lastTap != null ? 'Tapped' : '', + textAlign: TextAlign.center, + ))); columnChildren.add(Center( child: Text( lastLongPress, textAlign: TextAlign.center, ))); + columnChildren.add(Center( + child: Text( + _lastLongPress != null ? 'Long pressed' : '', + textAlign: TextAlign.center, + ))); } - return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, From a103bcd3327e1310aa67a89ed173caba81add36a Mon Sep 17 00:00:00 2001 From: Nick Bradshaw Date: Thu, 11 Nov 2021 08:08:04 -0800 Subject: [PATCH 157/259] [webview_flutter] Update webview_flutter documentation (#4481) --- packages/webview_flutter/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/webview_flutter/lib/src/webview.dart | 3 +++ packages/webview_flutter/webview_flutter/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 8f4826823c38..57d32efcab2b 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.1 + +* Add iOS-specific note to set `JavascriptMode.unrestricted` in order to set `zoomEnabled: false`. + ## 2.3.0 * Add ability to enable/disable zoom functionality. diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 442999d0ff5c..61702e514a4b 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -272,6 +272,9 @@ class WebView extends StatefulWidget { /// A Boolean value indicating whether the WebView should support zooming /// using its on-screen zoom controls and gestures. /// + /// *Note: On iOS [javascriptMode] must be set to + /// [JavascriptMode.unrestricted] in order to set [zoomEnabled] to false + /// /// By default 'zoomEnabled' is true final bool zoomEnabled; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index c7ac2e0200f9..b64e1b4818a3 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.3.0 +version: 2.3.1 environment: sdk: ">=2.14.0 <3.0.0" From 66ed8f4105ef037cbbb14f2770050a177d1e1e5c Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 12 Nov 2021 09:56:03 +0100 Subject: [PATCH 158/259] [webview_flutter] Implementations of `loadFile` and `loadHtmlString` for WKWebView (#4486) --- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/ios/RunnerTests/FLTWebViewTests.m | 203 ++++++++++++++++++ .../example/lib/main.dart | 60 ++++++ .../example/lib/web_view.dart | 29 +++ .../example/pubspec.yaml | 4 + .../ios/Classes/FlutterWebView.m | 89 ++++++++ .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 9 files changed, 393 insertions(+), 4 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index c1b19fd22d3e..cbf1f04a6181 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0 + +* Implemented new `loadFile` and `loadHtmlString` methods from the platform interface. + ## 2.3.0 * Implemented new `loadRequest` method from platform interface. 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 2be87fbf81be..e292b1bd52fa 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 @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -273,7 +273,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1030; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 68BDCAE823C3F7CB00D9C032 = { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d7453a8ce862..cb713d767632 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ '''; +const String kLocalFileExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + +'''; + class _WebViewExample extends StatefulWidget { const _WebViewExample({Key? key}) : super(key: key); @@ -121,6 +143,8 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadLocalFile, + loadHtmlString, doPostRequest, } @@ -159,6 +183,12 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case _MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; case _MenuOptions.doPostRequest: _onDoPostRequest(controller.data!, context); break; @@ -194,6 +224,14 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), const PopupMenuItem<_MenuOptions>( value: _MenuOptions.doPostRequest, child: Text('Post Request'), @@ -268,6 +306,18 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + void _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + void _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalFileExamplePage); + } + void _onDoPostRequest( WebViewController controller, BuildContext context) async { WebViewRequest request = WebViewRequest( @@ -292,6 +342,16 @@ class _SampleMenu extends StatelessWidget { children: cookieWidgets.toList(), ); } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + File indexFile = File('$tmpDir/www/index.html'); + + await Directory('$tmpDir/www').create(recursive: true); + await indexFile.writeAsString(kLocalFileExamplePage); + + return indexFile.path; + } } class _NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index b2555cd831c6..ab4b77336765 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -305,6 +305,35 @@ class WebViewController { WebView _widget; + /// Loads the file located on the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws an ArgumentError if the [absoluteFilePath] does not exist. + Future loadFile( + String absoluteFilePath, + ) { + assert(absoluteFilePath.isNotEmpty); + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + assert(html.isNotEmpty); + return _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 229da5e337a5..c8001c8ffde4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -8,6 +8,9 @@ environment: dependencies: flutter: sdk: flutter + + path_provider: ^2.0.6 + webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -31,3 +34,4 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + \ No newline at end of file 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 b8355ad18183..351d1ae58760 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -140,6 +140,10 @@ - (UIView*)view { - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([[call method] isEqualToString:@"updateSettings"]) { [self onUpdateSettings:call result:result]; + } else if ([[call method] isEqualToString:@"loadFile"]) { + [self onLoadFile:call result:result]; + } else if ([[call method] isEqualToString:@"loadHtmlString"]) { + [self onLoadHtmlString:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { [self onLoadUrl:call result:result]; } else if ([[call method] isEqualToString:@"loadRequest"]) { @@ -192,6 +196,60 @@ - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); } +- (void)onLoadFile:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* error = nil; + if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) { + result([FlutterError errorWithCode:@"loadFile_failed" + message:@"Failed parsing file path." + details:error]); + return; + } + + NSURL* url = [NSURL fileURLWithPath:[call arguments] isDirectory:NO]; + + if (!url) { + NSString* errorDetails = [NSString stringWithFormat:@"Initializing NSURL with the supplied " + @"'%@' path resulted in a nil value.", + [call arguments]]; + result([FlutterError errorWithCode:@"loadFile_failed" + message:@"Failed parsing file path." + details:errorDetails]); + return; + } + + NSURL* baseUrl = [url URLByDeletingLastPathComponent]; + + [_webView loadFileURL:url allowingReadAccessToURL:baseUrl]; + result(nil); +} + +- (void)onLoadHtmlString:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary* arguments = [call arguments]; + if (![arguments isKindOfClass:NSDictionary.class]) { + result([FlutterError + errorWithCode:@"loadHtmlString_failed" + message:@"Failed parsing arguments." + details:@"Arguments should be a dictionary containing at least a 'html' element and " + @"optionally a 'baseUrl' argument. For example: `@{ @\"html\": @\"some html " + @"code\", @\"baseUrl\": @\"https://flutter.dev\" }`"]); + return; + } + + NSString* htmlString = [call arguments][@"html"]; + NSString* baseUrl = + [call arguments][@"baseUrl"] == [NSNull null] ? nil : [call arguments][@"baseUrl"]; + NSString* error = nil; + if (![FLTWebViewController isValidStringArgument:htmlString withErrorMessage:&error]) { + result([FlutterError errorWithCode:@"loadHtmlString_failed" + message:@"Failed parsing HTML string argument." + details:error]); + return; + } + + [_webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:baseUrl]]; + result(nil); +} + - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { NSMutableDictionary* requestData = [[NSMutableDictionary alloc] init]; if (call.arguments[@"url"]) { @@ -556,6 +614,37 @@ - (void)updateUserAgent:(NSString*)userAgent { } } +/** + * Validates if the given `argument` is a non-null, non-empty string. + * + * @param argument The argument that should be validated. + * @param errorDetails An optional NSString variable which will contain a detailed error message in + * case the supplied argument is not valid. + * @return `YES` if the given `argument` is a valid non-null, non-empty string; otherwise `NO`. + */ ++ (BOOL)isValidStringArgument:(id)argument withErrorMessage:(NSString**)errorDetails { + if (!argument) { + if (errorDetails) { + *errorDetails = @"Argument is nil."; + } + return NO; + } + if (![argument isKindOfClass:NSString.class]) { + if (errorDetails) { + *errorDetails = @"Argument is not of type NSString."; + } + return NO; + } + if (![argument length]) { + if (errorDetails) { + *errorDetails = @"Argument contains an empty string."; + } + return NO; + } + + return YES; +} + #pragma mark WKUIDelegate - (WKWebView*)webView:(WKWebView*)webView diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index ff2b69fa8d20..466c1a2a4fcd 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.3.0 +version: 2.4.0 environment: sdk: ">=2.14.0 <3.0.0" From ace9e78bd3c33275ef6c3c2538d3f158b5cb3339 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Fri, 12 Nov 2021 11:04:46 -0800 Subject: [PATCH 159/259] [webview_flutter] Update Widget Unit Tests to be platform agnostic (#4489) --- .../webview_flutter/pubspec.yaml | 2 + .../test/webview_flutter_test.dart | 837 +++++++----------- .../test/webview_flutter_test.mocks.dart | 178 ++++ 3 files changed, 485 insertions(+), 532 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index b64e1b4818a3..4450837b1ede 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -24,8 +24,10 @@ dependencies: webview_flutter_wkwebview: ^2.2.0 dev_dependencies: + build_runner: ^2.1.5 flutter_driver: sdk: flutter flutter_test: sdk: flutter + mockito: ^5.0.16 pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index e325f68dc0b7..69d4142eb150 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -2,37 +2,47 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'webview_flutter_test.mocks.dart'; + typedef void VoidCallback(); +@GenerateMocks([WebViewPlatform, WebViewPlatformController]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); - - final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); - - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - SystemChannels.platform - .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); - }); + late MockWebViewPlatform mockWebViewPlatform; + late MockWebViewPlatformController mockWebViewPlatformController; setUp(() { - fakePlatformViewsController.reset(); - _fakeCookieManager.reset(); + mockWebViewPlatformController = MockWebViewPlatformController(); + mockWebViewPlatform = MockWebViewPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[Symbol('onWebViewPlatformCreated')]; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); + }); + + WebView.platform = mockWebViewPlatform; }); testWidgets('Create WebView', (WidgetTester tester) async { @@ -40,35 +50,41 @@ void main() { }); testWidgets('Initial url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); + await tester.pumpWidget(WebView(initialUrl: 'https://youtube.com')); - expect(await controller.currentUrl(), 'https://youtube.com'); + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; + + expect(params.initialUrl, 'https://youtube.com'); }); testWidgets('Javascript mode', (WidgetTester tester) async { await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams unrestrictedparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); + expect( + unrestrictedparams.webSettings!.javascriptMode, + JavascriptMode.unrestricted, + ); await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.disabled, )); - expect(platformWebView.javascriptMode, JavascriptMode.disabled); + + final CreationParams disabledparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; + + expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); }); testWidgets('Load url', (WidgetTester tester) async { @@ -85,7 +101,10 @@ void main() { await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + argThat(isNull), + )); }); testWidgets('Invalid urls', (WidgetTester tester) async { @@ -100,14 +119,15 @@ void main() { expect(controller, isNotNull); - expect(await controller!.currentUrl(), isNull); + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(() => controller!.loadUrl(''), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); + expect(params.initialUrl, isNull); - // Missing schema. + expect(() => controller!.loadUrl(''), throwsA(anything)); expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); }); testWidgets('Headers in loadUrl', (WidgetTester tester) async { @@ -126,25 +146,11 @@ void main() { 'CACHE-CONTROL': 'ABC' }; await controller!.loadUrl('https://flutter.io', headers: headers); - expect(await controller!.currentUrl(), equals('https://flutter.io')); - }); - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoBackNoPageLoaded = await controller!.canGoBack(); - - expect(canGoBackNoPageLoaded, false); + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + {'CACHE-CONTROL': 'ABC'}, + )); }); testWidgets("Clear Cache", (WidgetTester tester) async { @@ -158,68 +164,16 @@ void main() { ); expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); await controller!.clearCache(); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); - }); - - testWidgets("Can't go back with no history", (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); - - expect(canGoBackFirstPageLoaded, false); + verify(mockWebViewPlatformController.clearCache()); }); testWidgets('Can go back', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); - - expect(canGoBackSecondPageLoaded, true); - }); - - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); + when(mockWebViewPlatformController.canGoBack()) + .thenAnswer((_) => Future.value(true)); - expect(canGoForwardNoPageLoaded, false); - }); - - testWidgets("Can't go forward with no history", (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -231,16 +185,16 @@ void main() { ); expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); - - expect(canGoForwardFirstPageLoaded, false); + expect(controller!.canGoBack(), completion(true)); }); - testWidgets('Can go forward', (WidgetTester tester) async { + testWidgets("Can't go forward", (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoForward()) + .thenAnswer((_) => Future.value(false)); + WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -248,12 +202,7 @@ void main() { ); expect(controller, isNotNull); - - await controller!.loadUrl('https://youtube.com'); - await controller!.goBack(); - final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); - - expect(canGoForwardFirstPageBacked, true); + expect(controller!.canGoForward(), completion(false)); }); testWidgets('Go back', (WidgetTester tester) async { @@ -267,17 +216,8 @@ void main() { ), ); - expect(controller, isNotNull); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); + verify(mockWebViewPlatformController.goBack()); }); testWidgets('Go forward', (WidgetTester tester) async { @@ -292,23 +232,14 @@ void main() { ); expect(controller, isNotNull); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller!.goForward(); - - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.goForward()); }); testWidgets('Current URL', (WidgetTester tester) async { + when(mockWebViewPlatformController.currentUrl()) + .thenAnswer((_) => Future.value('https://youtube.com')); + WebViewController? controller; await tester.pumpWidget( WebView( @@ -319,17 +250,6 @@ void main() { ); expect(controller, isNotNull); - - // Test a WebView without an explicitly set first URL. - expect(await controller!.currentUrl(), isNull); - - await controller!.loadUrl('https://youtube.com'); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); expect(await controller!.currentUrl(), 'https://youtube.com'); }); @@ -344,23 +264,14 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - await controller.reload(); - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); - - await controller.loadUrl('https://youtube.com'); - - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + verify(mockWebViewPlatformController.reload()); }); testWidgets('evaluate Javascript', (WidgetTester tester) async { + when(mockWebViewPlatformController.evaluateJavascript('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + late WebViewController controller; await tester.pumpWidget( WebView( @@ -371,10 +282,11 @@ void main() { }, ), ); + expect( // ignore: deprecated_member_use_from_same_package - await controller.evaluateJavascript("fake js string"), - "fake js string", + await controller.evaluateJavascript('fake js string'), + 'fake js string', reason: 'should get the argument'); }); @@ -409,8 +321,7 @@ void main() { ), ); await controller.runJavascript('fake js string'); - expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString, - 'fake js string'); + verify(mockWebViewPlatformController.runJavascript('fake js string')); }); testWidgets('runJavaScript with JavascriptMode disabled', @@ -432,6 +343,10 @@ void main() { }); testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + when(mockWebViewPlatformController + .runJavascriptReturningResult('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + late WebViewController controller; await tester.pumpWidget( WebView( @@ -442,8 +357,8 @@ void main() { }, ), ); - expect(await controller.runJavascriptReturningResult("fake js string"), - "fake js string", + expect(await controller.runJavascriptReturningResult('fake js string'), + 'fake js string', reason: 'should get the argument'); }); @@ -466,18 +381,9 @@ void main() { }); testWidgets('Cookies can be cleared once', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - }); + when(mockWebViewPlatform.clearCookies()) + .thenAnswer((_) => Future.value(true)); - testWidgets('Second cookie clear does not have cookies', - (WidgetTester tester) async { await tester.pumpWidget( const WebView( initialUrl: 'https://flutter.io', @@ -486,8 +392,6 @@ void main() { final CookieManager cookieManager = CookieManager(); final bool hasCookies = await cookieManager.clearCookies(); expect(hasCookies, true); - final bool hasCookiesSecond = await cookieManager.clearCookies(); - expect(hasCookiesSecond, false); }); testWidgets('Initial JavaScript channels', (WidgetTester tester) async { @@ -503,10 +407,12 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.javascriptChannelNames, + expect(params.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); }); @@ -570,11 +476,15 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).first; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + expect( + channelRegistry.channels.keys, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3']), + ); }); testWidgets('Remove all JavaScript channels and then add', @@ -609,11 +519,12 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).last; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts'])); + expect(channelRegistry.channels.keys, unorderedEquals(['Tts'])); }); testWidgets('JavaScript channel messages', (WidgetTester tester) async { @@ -637,14 +548,16 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).single; expect(ttsMessagesReceived, isEmpty); expect(alarmMessagesReceived, isEmpty); - platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); - platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + channelRegistry.onJavascriptChannelMessage('Tts', 'Hello'); + channelRegistry.onJavascriptChannelMessage('Tts', 'World'); expect(ttsMessagesReceived, ['Hello', 'World']); }); @@ -660,12 +573,14 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageStarted is null', (WidgetTester tester) async { @@ -674,12 +589,14 @@ void main() { onPageStarted: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // The platform side will always invoke a call for onPageStarted. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); }); testWidgets('onPageStarted changed', (WidgetTester tester) async { @@ -697,12 +614,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageStartedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -717,12 +635,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; + handler.onPageFinished('https://youtube.com'); - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageFinished is null', (WidgetTester tester) async { @@ -731,12 +650,13 @@ void main() { onPageFinished: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // The platform side will always invoke a call for onPageFinished. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageFinishedCallback(); + handler.onPageFinished('https://youtube.com'); }); testWidgets('onPageFinished changed', (WidgetTester tester) async { @@ -754,12 +674,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onPageFinished('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -774,10 +695,11 @@ void main() { }, )); - final FakePlatformWebView? platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView?.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -788,11 +710,13 @@ void main() { onProgress: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // This is to test that it does not crash on a null callback. - platformWebView.fakeOnProgressCallback(50); + handler.onProgress(50); }); testWidgets('onLoadingProgress changed', (WidgetTester tester) async { @@ -810,10 +734,11 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -825,10 +750,12 @@ void main() { initialUrl: 'https://youtube.com', )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.hasNavigationDelegate, false); + expect(params.webSettings!.hasNavigationDelegate, false); await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -836,7 +763,12 @@ void main() { NavigationDecision.navigate, )); - expect(platformWebView.hasNavigationDelegate, true); + final WebSettings updateSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .single; + + expect(updateSettings.hasNavigationDelegate, true); }); testWidgets('Block navigation', (WidgetTester tester) async { @@ -852,22 +784,38 @@ void main() { : NavigationDecision.prevent; })); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final List args = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + webViewPlatformCallbacksHandler: true, + ); + + final CreationParams params = args[0]; + expect(params.webSettings!.hasNavigationDelegate, true); - expect(platformWebView.hasNavigationDelegate, true); + final WebViewPlatformCallbacksHandler handler = args[1]; - platformWebView.fakeNavigate('https://www.google.com'); // The navigation delegate only allows navigation to https://flutter.dev // so we should still be in https://youtube.com. - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect( + handler.onNavigationRequest( + url: 'https://www.google.com', + isForMainFrame: true, + ), + completion(false), + ); + expect(navigationRequests.length, 1); expect(navigationRequests[0].url, 'https://www.google.com'); expect(navigationRequests[0].isForMainFrame, true); - platformWebView.fakeNavigate('https://flutter.dev'); - await tester.pump(); - expect(platformWebView.currentUrl, 'https://flutter.dev'); + expect( + handler.onNavigationRequest( + url: 'https://flutter.dev', + isForMainFrame: true, + ), + completion(true), + ); }); }); @@ -877,41 +825,50 @@ void main() { debuggingEnabled: true, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.debuggingEnabled, true); + expect(params.webSettings!.debuggingEnabled, true); }); testWidgets('defaults to false', (WidgetTester tester) async { await tester.pumpWidget(const WebView()); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.debuggingEnabled, false); + expect(params.webSettings!.debuggingEnabled, false); }); testWidgets('can be changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(WebView(key: key)); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - await tester.pumpWidget(WebView( key: key, debuggingEnabled: true, )); - expect(platformWebView.debuggingEnabled, true); + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(enabledSettings.debuggingEnabled, true); await tester.pumpWidget(WebView( key: key, debuggingEnabled: false, )); - expect(platformWebView.debuggingEnabled, false); + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(disabledSettings.debuggingEnabled, false); }); }); @@ -921,46 +878,56 @@ void main() { zoomEnabled: true, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.zoomEnabled, isTrue); + expect(params.webSettings!.zoomEnabled, isTrue); }); testWidgets('defaults to true', (WidgetTester tester) async { await tester.pumpWidget(const WebView()); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.zoomEnabled, isTrue); + expect(params.webSettings!.zoomEnabled, isTrue); }); testWidgets('can be changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(WebView(key: key)); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - await tester.pumpWidget(WebView( key: key, zoomEnabled: true, )); - expect(platformWebView.zoomEnabled, isTrue); + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + // Zoom defaults to true, so no changes are made to settings. + expect(enabledSettings.zoomEnabled, isNull); await tester.pumpWidget(WebView( key: key, zoomEnabled: false, )); - expect(platformWebView.zoomEnabled, isFalse); + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(disabledSettings.zoomEnabled, isFalse); }); }); group('Custom platform implementation', () { - setUpAll(() { + setUp(() { WebView.platform = MyWebViewPlatform(); }); tearDownAll(() { @@ -1017,16 +984,19 @@ void main() { expect(platform.lastRequestHeaders, headers); }); }); + testWidgets('Set UserAgent', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.userAgent, isNull); + expect(params.webSettings!.userAgent.value, isNull); await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', @@ -1034,264 +1004,74 @@ void main() { userAgent: 'UA', )); - expect(platformWebView.userAgent, 'UA'); + final WebSettings settings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(settings.userAgent.value, 'UA'); }); } -class FakePlatformWebView { - FakePlatformWebView(int? id, Map params) { - if (params.containsKey('initialUrl')) { - final String? initialUrl = params['initialUrl']; - if (initialUrl != null) { - history.add(initialUrl); - currentPosition++; - } - } - if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = - List.from(params['javascriptChannelNames']); - } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; - hasNavigationDelegate = - params['settings']['hasNavigationDelegate'] ?? false; - debuggingEnabled = params['settings']['debuggingEnabled']; - userAgent = params['settings']['userAgent']; - zoomEnabled = params['settings']['zoomEnabled'] ?? true; - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); - channel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel channel; - - List history = []; - int currentPosition = -1; - int amountOfReloadsOnCurrentUrl = 0; - bool hasCache = true; - - String? get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode? javascriptMode; - List? javascriptChannelNames; - - bool? hasNavigationDelegate; - bool? debuggingEnabled; - String? userAgent; - bool? zoomEnabled; - - String? lastRunJavaScriptString; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'loadUrl': - final Map request = call.arguments; - _loadUrl(request['url']); - return Future.sync(() {}); - case 'updateSettings': - if (call.arguments['jsMode'] != null) { - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; - } - if (call.arguments['hasNavigationDelegate'] != null) { - hasNavigationDelegate = call.arguments['hasNavigationDelegate']; - } - if (call.arguments['debuggingEnabled'] != null) { - debuggingEnabled = call.arguments['debuggingEnabled']; - } - userAgent = call.arguments['userAgent']; - if (call.arguments['zoomEnabled'] != null) { - zoomEnabled = call.arguments['zoomEnabled']; - } - break; - case 'canGoBack': - return Future.sync(() => currentPosition > 0); - case 'canGoForward': - return Future.sync(() => currentPosition < history.length - 1); - case 'goBack': - currentPosition = max(-1, currentPosition - 1); - return Future.sync(() {}); - case 'goForward': - currentPosition = min(history.length - 1, currentPosition + 1); - return Future.sync(() {}); - case 'reload': - amountOfReloadsOnCurrentUrl++; - return Future.sync(() {}); - case 'currentUrl': - return Future.value(currentUrl); - case 'runJavascriptReturningResult': - case 'evaluateJavascript': - lastRunJavaScriptString = call.arguments; - return Future.value(call.arguments); - case 'runJavascript': - lastRunJavaScriptString = call.arguments; - return Future.sync(() {}); - case 'addJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames!.addAll(channelNames); - break; - case 'removeJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames! - .removeWhere((String channel) => channelNames.contains(channel)); - break; - case 'clearCache': - hasCache = false; - return Future.sync(() {}); - } - return Future.sync(() {}); - } - - void fakeJavascriptPostMessage(String jsChannel, String message) { - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'channel': jsChannel, - 'message': message - }; - final ByteData data = codec - .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - // Fakes a main frame navigation that was initiated by the webview, e.g when - // the user clicks a link in the currently loaded page. - void fakeNavigate(String url) { - if (!hasNavigationDelegate!) { - print('no navigation delegate'); - _loadUrl(url); - return; - } - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'url': url, - 'isForMainFrame': true - }; - final ByteData data = - codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) { - final bool allow = codec.decodeEnvelope(data!); - if (allow) { - _loadUrl(url); - } - }); - } - - void fakeOnPageStartedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageStarted', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnPageFinishedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageFinished', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnProgressCallback(int progress) { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onProgress', - {'progress': progress}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - void _loadUrl(String? url) { - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; - } +List captureBuildArgs( + MockWebViewPlatform mockWebViewPlatform, { + bool context = false, + bool creationParams = false, + bool webViewPlatformCallbacksHandler = false, + bool javascriptChannelRegistry = false, + bool onWebViewPlatformCreated = false, + bool gestureRecognizers = false, +}) { + return verify(mockWebViewPlatform.build( + context: context ? captureAnyNamed('context') : anyNamed('context'), + creationParams: creationParams + ? captureAnyNamed('creationParams') + : anyNamed('creationParams'), + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler + ? captureAnyNamed('webViewPlatformCallbacksHandler') + : anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: javascriptChannelRegistry + ? captureAnyNamed('javascriptChannelRegistry') + : anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: onWebViewPlatformCreated + ? captureAnyNamed('onWebViewPlatformCreated') + : anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: gestureRecognizers + ? captureAnyNamed('gestureRecognizers') + : anyNamed('gestureRecognizers'), + )).captured; } -class _FakePlatformViewsController { - FakePlatformWebView? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params'])!; - lastCreatedView = FakePlatformWebView( - args['id'], - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } +// This Widget ensures that onWebViewPlatformCreated is only called once when +// making multiple calls to `WidgetTester.pumpWidget` with different parameters +// for the WebView. +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + Key? key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }) : super(key: key); - void reset() { - lastCreatedView = null; - } -} + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes); + @override + State createState() => TestPlatformWebViewState(); } -class _FakeCookieManager { - _FakeCookieManager() { - final MethodChannel channel = const MethodChannel( - 'plugins.flutter.io/cookie_manager', - StandardMethodCodec(), - ); - channel.setMockMethodCallHandler(onMethodCall); - } - - bool hasCookies = true; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'clearCookies': - bool hadCookies = false; - if (hasCookies) { - hadCookies = true; - hasCookies = false; - } - return Future.sync(() { - return hadCookies; - }); +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); } - return Future.sync(() => true); } - void reset() { - hasCookies = true; + @override + Widget build(BuildContext context) { + return Container(); } } @@ -1381,10 +1161,3 @@ class MatchesCreationParams extends Matcher { .matches(creationParams.javascriptChannelNames, matchState); } } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart new file mode 100644 index 000000000000..b9d43cc6f182 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart @@ -0,0 +1,178 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter/test/webview_flutter_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i9; + +import 'package:flutter/foundation.dart' as _i7; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/widgets.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart' + as _i3; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' + as _i5; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart' + as _i10; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i4; + +// 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 _FakeWidget_0 extends _i1.Fake implements _i2.Widget { + @override + String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i3.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget build( + {_i2.BuildContext? context, + _i4.CreationParams? creationParams, + _i5.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + _i6.JavascriptChannelRegistry? javascriptChannelRegistry, + _i3.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i7.Factory<_i8.OneSequenceGestureRecognizer>>? + gestureRecognizers}) => + (super.noSuchMethod( + Invocation.method(#build, [], { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers + }), + returnValue: _FakeWidget_0()) as _i2.Widget); + @override + _i9.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i9.Future); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i10.WebViewPlatformController { + MockWebViewPlatformController() { + _i1.throwOnMissingStub(this); + } + + @override + _i9.Future loadUrl(String? url, Map? headers) => + (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future updateSettings(_i4.WebSettings? setting) => + (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future currentUrl() => + (super.noSuchMethod(Invocation.method(#currentUrl, []), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future clearCache() => + (super.noSuchMethod(Invocation.method(#clearCache, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]), + returnValue: Future.value('')) as _i9.Future); + @override + _i9.Future runJavascript(String? javascript) => + (super.noSuchMethod(Invocation.method(#runJavascript, [javascript]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method(#runJavascriptReturningResult, [javascript]), + returnValue: Future.value('')) as _i9.Future); + @override + _i9.Future addJavascriptChannels(Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method(#addJavascriptChannels, [javascriptChannelNames]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, [javascriptChannelNames]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future scrollTo(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future scrollBy(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getScrollX() => + (super.noSuchMethod(Invocation.method(#getScrollX, []), + returnValue: Future.value(0)) as _i9.Future); + @override + _i9.Future getScrollY() => + (super.noSuchMethod(Invocation.method(#getScrollY, []), + returnValue: Future.value(0)) as _i9.Future); + @override + String toString() => super.toString(); +} From 8a95012fb0376a60d293b3b1fcdca4a474611b4c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 12 Nov 2021 19:35:04 -0500 Subject: [PATCH 160/259] [path_provider] Publish fully federated version (#4497) Now that the iOS and Android implementation packages are published, switch to them and re-enable publishing. Part of https://github.com/flutter/flutter/issues/68498 --- packages/path_provider/path_provider/pubspec.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 8757462a449c..a31e804fa357 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -3,9 +3,6 @@ description: Flutter plugin for getting commonly used locations on host platform repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 version: 2.0.7 -# Temporarily disable publishing to allow moving Android and iOS -# implementations. -publish_to: none environment: sdk: ">=2.14.0 <3.0.0" @@ -28,11 +25,8 @@ flutter: dependencies: flutter: sdk: flutter - # Temporary path dependencies to allow moving Android and iOS implementations. - path_provider_android: - path: ../path_provider_android - path_provider_ios: - path: ../path_provider_ios + path_provider_android: ^2.0.6 + path_provider_ios: ^2.0.6 path_provider_linux: ^2.0.0 path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 From 31268aabbaa96758d65862e05a7bd92925019ad3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 17:43:17 -0500 Subject: [PATCH 161/259] [battery] Recreate Android example (#4508) Re-creates example/android using `flutter create`, preserving only the integration test harness files and the gradle settings, since the example was using the v1 embedding, which is now an error on Flutter master. --- packages/battery/battery/CHANGELOG.md | 1 + .../battery/example/android/.gitignore | 13 +++++++ .../gradle/wrapper/gradle-wrapper.properties | 5 --- .../android/app/src/debug/AndroidManifest.xml | 7 ++++ .../android/app/src/main/AndroidManifest.xml | 39 ++++++++++++++----- .../res/drawable-v21/launch_background.xml | 12 ++++++ .../main/res/drawable/launch_background.xml | 12 ++++++ .../app/src/main/res/values-night/styles.xml | 18 +++++++++ .../app/src/main/res/values/styles.xml | 18 +++++++++ .../app/src/profile/AndroidManifest.xml | 7 ++++ .../battery/example/android/build.gradle | 4 +- .../battery/example/android/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../battery/example/android/settings.gradle | 18 ++++----- 14 files changed, 127 insertions(+), 31 deletions(-) create mode 100644 packages/battery/battery/example/android/.gitignore delete mode 100644 packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/battery/battery/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/battery/battery/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/battery/battery/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/battery/battery/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/battery/battery/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/battery/battery/example/android/app/src/profile/AndroidManifest.xml diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index ddc912d2ba2a..d78c5d3eaf23 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove references to the Android v1 embedding. * Updated Android lint settings. +* Recreated Android example from current template. ## 2.0.3 diff --git a/packages/battery/battery/example/android/.gitignore b/packages/battery/battery/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/battery/battery/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/battery/battery/example/android/app/src/debug/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..a54ab831b1db --- /dev/null +++ b/packages/battery/battery/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml index 11feb41de96a..a84b616ccc9c 100644 --- a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml +++ b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml @@ -1,20 +1,39 @@ - - - - - + + + + + + + diff --git a/packages/battery/battery/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/battery/battery/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/battery/battery/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/battery/battery/example/android/app/src/main/res/drawable/launch_background.xml b/packages/battery/battery/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/battery/battery/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/battery/battery/example/android/app/src/main/res/values-night/styles.xml b/packages/battery/battery/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..3db14bb5391f --- /dev/null +++ b/packages/battery/battery/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/battery/battery/example/android/app/src/main/res/values/styles.xml b/packages/battery/battery/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d460d1e9215b --- /dev/null +++ b/packages/battery/battery/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/battery/battery/example/android/app/src/profile/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..a54ab831b1db --- /dev/null +++ b/packages/battery/battery/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/battery/battery/example/android/build.gradle b/packages/battery/battery/example/android/build.gradle index e101ac08df55..622ddc5c15ef 100644 --- a/packages/battery/battery/example/android/build.gradle +++ b/packages/battery/battery/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:4.1.0' } } @@ -19,8 +19,6 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } diff --git a/packages/battery/battery/example/android/gradle.properties b/packages/battery/battery/example/android/gradle.properties index 38c8d4544ff1..94adc3a3f97a 100644 --- a/packages/battery/battery/example/android/gradle.properties +++ b/packages/battery/battery/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..bc6a58afdda2 100644 --- a/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/battery/battery/example/android/settings.gradle b/packages/battery/battery/example/android/settings.gradle index 115da6cb4f4d..44e62bcf06ae 100644 --- a/packages/battery/battery/example/android/settings.gradle +++ b/packages/battery/battery/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" From dbcfe062acec341f0a0e068cd4c14b11f11d4b4b Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Sun, 14 Nov 2021 15:16:03 -0800 Subject: [PATCH 162/259] Enable camera/quick_actions tests (#4506) --- script/configs/exclude_native_unit_android.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml index 1437e8b23c62..5ec80eee73a0 100644 --- a/script/configs/exclude_native_unit_android.yaml +++ b/script/configs/exclude_native_unit_android.yaml @@ -9,7 +9,3 @@ # No need for unit tests: - espresso - -# TODO(egarciad): Re-enable these tests https://github.com/flutter/flutter/issues/93286 -- camera/camera -- quick_actions/quick_actions From 027e940b89a520ffa4f772d0e543813eaf5dd902 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 18:30:58 -0500 Subject: [PATCH 163/259] [ci] Add 'main' builders in bringup mode (#4473) Creates `main` versions of the existing `master` CI builders, as the first half of switching LUCI tests over to the `main` branch. Part of https://github.com/flutter/flutter/issues/90476 --- .ci.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 2cd36d5b36dd..32e309cf473e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -48,6 +48,20 @@ targets: ] scheduler: luci + - name: Windows win32-platform_tests main + bringup: true + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: windows_build_and_platform_tests.yaml + channel: main + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + - name: Windows win32-platform_tests stable recipe: plugins/plugins timeout: 30 @@ -73,6 +87,20 @@ targets: ] scheduler: luci + - name: Windows windows-build_all_plugins main + bringup: true + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: build_all_plugins.yaml + channel: main + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + - name: Windows windows-build_all_plugins stable recipe: plugins/plugins timeout: 30 @@ -98,6 +126,20 @@ targets: ] scheduler: luci + - name: Windows uwp-platform_tests main + bringup: true + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: uwp_build_and_platform_tests.yaml + channel: main + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + - name: Windows plugin_tools_tests recipe: plugins/plugins timeout: 30 From d90b64f330e55591e2eb6955b120919bb2f39ad8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 20:21:32 -0500 Subject: [PATCH 164/259] [flutter_plugin_tools] Add optional timing info (#4500) Adds a `--log-timing` flag that can be passed to cause package-looping commands to log relative start time and per-package elapsed time, to help with future efforts to improve CI times (e.g., finding unusually slow packages, or designing changes to sharding). Adds this flag to the CI scripts to enable timing there. --- .ci/scripts/build_examples_uwp.sh | 2 +- .ci/scripts/build_examples_win32.sh | 2 +- .ci/scripts/drive_examples_win32.sh | 2 +- .ci/scripts/native_test_win32.sh | 2 +- script/tool/CHANGELOG.md | 5 ++ .../src/common/package_looping_command.dart | 58 ++++++++++++++----- .../tool/lib/src/common/plugin_command.dart | 30 ++++++---- .../common/package_looping_command_test.dart | 41 +++++++++++++ script/tool_runner.sh | 2 +- 9 files changed, 116 insertions(+), 28 deletions(-) diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh index 639cb054e4b7..04b8256891bd 100644 --- a/.ci/scripts/build_examples_uwp.sh +++ b/.ci/scripts/build_examples_uwp.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --winuwp \ - --packages-for-branch + --packages-for-branch --log-timing diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index 8c090f4b78d2..bcf57a4b311f 100644 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ - --packages-for-branch + --packages-for-branch --log-timing diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index 63abc06bec5a..67f4fde5a70a 100644 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ - --packages-for-branch + --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 938515784412..37cf54e55c5c 100644 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ - --no-integration --packages-for-branch + --no-integration --packages-for-branch --log-timing diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index aa931391a93a..5a037c89c617 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Added `--log-timing` to add timing information to package headers in looping + commands. + ## 0.7.2 - Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 973ac9995cb8..bfee71a0f4c2 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -170,7 +170,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// messages. DO NOT RELY on someone noticing a warning; instead, use it for /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { - print(Colorize(warningMessage)..yellow()); + _printColorized(warningMessage, Styles.YELLOW); if (_currentPackageEntry != null) { _packagesWithWarnings.add(_currentPackageEntry!); } else { @@ -219,6 +219,8 @@ abstract class PackageLoopingCommand extends PluginCommand { _otherWarningCount = 0; _currentPackageEntry = null; + final DateTime runStart = DateTime.now(); + await initializeRun(); final List targetPackages = @@ -227,8 +229,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final Map results = {}; for (final PackageEnumerationEntry entry in targetPackages) { + final DateTime packageStart = DateTime.now(); _currentPackageEntry = entry; - _printPackageHeading(entry); + _printPackageHeading(entry, startTime: runStart); // Command implementations should never see excluded packages; they are // included at this level only for logging. @@ -246,11 +249,20 @@ abstract class PackageLoopingCommand extends PluginCommand { result = PackageResult.fail(['Unhandled exception']); } if (result.state == RunState.skipped) { - final String message = - '${indentation}SKIPPING: ${result.details.first}'; - captureOutput ? print(message) : print(Colorize(message)..darkGray()); + _printColorized('${indentation}SKIPPING: ${result.details.first}', + Styles.DARK_GRAY); } results[entry] = result; + + // Only log an elapsed time for long output; for short output, comparing + // the relative timestamps of successive entries should be trivial. + if (shouldLogTiming && hasLongOutput) { + final Duration elapsedTime = DateTime.now().difference(packageStart); + _printColorized( + '\n[${entry.package.displayName} completed in ' + '${elapsedTime.inMinutes}m ${elapsedTime.inSeconds % 60}s]', + Styles.DARK_GRAY); + } } _currentPackageEntry = null; @@ -287,11 +299,20 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry entry) { + void _printPackageHeading(PackageEnumerationEntry entry, + {required DateTime startTime}) { final String packageDisplayName = entry.package.displayName; String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; + + if (shouldLogTiming) { + final Duration relativeTime = DateTime.now().difference(startTime); + final String timeString = _formatDurationAsRelativeTime(relativeTime); + heading = + hasLongOutput ? '$heading [@$timeString]' : '[$timeString] $heading'; + } + if (hasLongOutput) { heading = ''' @@ -302,13 +323,7 @@ abstract class PackageLoopingCommand extends PluginCommand { } else if (!entry.excluded) { heading = '$heading...'; } - if (captureOutput) { - print(heading); - } else { - final Colorize colorizeHeading = Colorize(heading); - print( - entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); - } + _printColorized(heading, entry.excluded ? Styles.DARK_GRAY : Styles.CYAN); } /// Prints a summary of packges run, packages skipped, and warnings. @@ -401,4 +416,21 @@ abstract class PackageLoopingCommand extends PluginCommand { } _printError(failureListFooter); } + + /// Prints [message] in [color] unless [captureOutput] is set, in which case + /// it is printed without color. + void _printColorized(String message, Styles color) { + if (captureOutput) { + print(message); + } else { + print(Colorize(message)..apply(color)); + } + } + + /// Returns a duration [d] formatted as minutes:seconds. Does not use hours, + /// since time logging is primarily intended for CI, where durations should + /// always be less than an hour. + String _formatDurationAsRelativeTime(Duration d) { + return '${d.inMinutes}:${(d.inSeconds % 60).toString().padLeft(2, '0')}'; + } } diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 6708f9101130..f40a102dfbc0 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -76,7 +76,7 @@ abstract class PluginCommand extends Command { 'If no packages have changed, or if there have been changes that may\n' 'affect all packages, the command runs on all packages.\n' 'The packages excluded with $_excludeArg is also excluded even if changed.\n' - 'See $_kBaseSha if a custom base is needed to determine the diff.\n\n' + 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); argParser.addFlag(_packagesForBranchArg, help: @@ -86,20 +86,25 @@ abstract class PluginCommand extends Command { 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); - argParser.addOption(_kBaseSha, + argParser.addOption(_baseShaArg, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' 'If not specified, merge-base is used as base sha.'); + argParser.addFlag(_logTimingArg, + help: 'Logs timing information.\n\n' + 'Currently only logs per-package timing for multi-package commands, ' + 'but more information may be added in the future.'); } - static const String _pluginsArg = 'plugins'; - static const String _packagesArg = 'packages'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; + static const String _baseShaArg = 'base-sha'; static const String _excludeArg = 'exclude'; - static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _logTimingArg = 'log-timing'; + static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; - static const String _kBaseSha = 'base-sha'; + static const String _pluginsArg = 'plugins'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _shardCountArg = 'shardCount'; + static const String _shardIndexArg = 'shardIndex'; /// The directory containing the plugin packages. final Directory packagesDir; @@ -183,6 +188,11 @@ abstract class PluginCommand extends Command { return (argResults![key] as List?) ?? []; } + /// If true, commands should log timing information that might be useful in + /// analyzing their runtime (e.g., the per-package time for multi-package + /// commands). + bool get shouldLogTiming => getBoolArg(_logTimingArg); + void _checkSharding() { final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); @@ -411,11 +421,11 @@ abstract class PluginCommand extends Command { return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); } - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. Future retrieveVersionFinder() async { - final String baseSha = getStringArg(_kBaseSha); + final String baseSha = getStringArg(_baseShaArg); final GitVersionFinder gitVersionFinder = GitVersionFinder(await gitDir, baseSha); diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 7cf03960a74d..6e46a3330cc8 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -22,6 +22,7 @@ import '../util.dart'; import 'plugin_command_test.mocks.dart'; // Constants for colorized output start and end. +const String _startElapsedTimeColor = '\x1B[90m'; const String _startErrorColor = '\x1B[31m'; const String _startHeadingColor = '\x1B[36m'; const String _startSkipColor = '\x1B[90m'; @@ -272,6 +273,46 @@ void main() { ])); }); + test('prints timing info in long-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + const String separator = + '============================================================'; + expect( + output, + containsAllInOrder([ + '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', + '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', + ])); + }); + + test('prints timing info in short-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + expect( + output, + containsAllInOrder([ + '$_startHeadingColor[0:00] Running for package_a...$_endColor', + '$_startHeadingColor[0:00] Running for package_b...$_endColor', + ])); + // Short-form output should not include elapsed time. + expect(output, isNot(contains('[package_a completed in 0m 0s]'))); + }); + test('shows the success message when nothing fails', () async { createFakePackage('package_a', packagesDir); createFakePackage('package_b', packagesDir); diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 99bab387e6b6..66181543f2cf 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -19,4 +19,4 @@ readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" # The tool expects to be run from the repo root. cd "$REPO_DIR" # Run from the in-tree source. -dart run "$TOOL_PATH" "$@" --packages-for-branch $PLUGIN_SHARDING +dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PLUGIN_SHARDING From 8d0382e0903dfe2b8a9b2ee4dd287c29399fda6b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 14 Nov 2021 21:26:05 -0500 Subject: [PATCH 165/259] [flutter_plugin_tools] Build gtest unit tests (#4492) --- .../example/linux/flutter/CMakeLists.txt | 3 +- .../flutter/generated_plugin_registrant.cc | 6 +- .../example/linux/CMakeLists.txt | 2 + .../example/windows/CMakeLists.txt | 2 + script/tool/CHANGELOG.md | 2 + script/tool/lib/src/common/cmake.dart | 118 ++++++++++++ script/tool/lib/src/common/gradle.dart | 3 - script/tool/lib/src/native_test_command.dart | 69 +++++-- .../tool/test/native_test_command_test.dart | 172 +++++++++++++++--- 9 files changed, 333 insertions(+), 44 deletions(-) create mode 100644 script/tool/lib/src/common/cmake.dart diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt index 94f43ff7fa6a..33fd5801e713 100644 --- a/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt +++ b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt @@ -78,7 +78,8 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - linux-x64 ${CMAKE_BUILD_TYPE} + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc index d9fdd53925c5..4f7884874da7 100644 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt index 1758aac03b0d..11219aa55928 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt @@ -45,6 +45,8 @@ add_dependencies(${BINARY_NAME} flutter_assemble) # Enable the test target. set(include_url_launcher_linux_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS url_launcher_linux_test) # Generated plugin build rules, which manage building the plugins and adding # them to the application. diff --git a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt index 5b1622bcb333..5a5d2e8034b2 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -48,6 +48,8 @@ add_subdirectory("runner") # Enable the test target. set(include_url_launcher_windows_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS url_launcher_windows_test) # Generated plugin build rules, which manage building the plugins and adding # them to the application. diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 5a037c89c617..31efc28aa3ea 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,7 @@ ## NEXT +- `native-test` now builds unit tests before running them on Windows and Linux, + matching the behavior of other platforms. - Added `--log-timing` to add timing information to package headers in looping commands. diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart new file mode 100644 index 000000000000..04ad880292b9 --- /dev/null +++ b/script/tool/lib/src/common/cmake.dart @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; + +/// A utility class for interacting with CMake projects. +class CMakeProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + CMakeProject( + this.flutterProject, { + required this.buildMode, + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The build mode (e.g., Debug, Release). + /// + /// This is a constructor paramater because on Linux many properties depend + /// on the build mode since it uses a single-configuration generator. + final String buildMode; + + late final String _cmakeCommand = _determineCmakeCommand(); + + /// The project's platform directory name. + String get _platformDirName => platform.isWindows ? 'windows' : 'linux'; + + /// The project's 'example' build directory for this instance's platform. + Directory get buildDirectory { + Directory buildDir = + flutterProject.childDirectory('build').childDirectory(_platformDirName); + if (platform.isLinux) { + buildDir = buildDir + // TODO(stuartmorgan): Support arm64 if that ever becomes a supported + // CI configuration for the repository. + .childDirectory('x64') + // Linux uses a single-config generator, so the base build directory + // includes the configuration. + .childDirectory(buildMode.toLowerCase()); + } + return buildDir; + } + + File get _cacheFile => buildDirectory.childFile('CMakeCache.txt'); + + /// Returns the CMake command to run build commands for this project. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String getCmakeCommand() { + return _cmakeCommand; + } + + /// Returns the CMake command to run build commands for this project. This is + /// used to initialize _cmakeCommand, and should not be called directly. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String _determineCmakeCommand() { + // On Linux 'cmake' is expected to be in the path, so doesn't need to + // be lookup up and cached. + if (platform.isLinux) { + return 'cmake'; + } + final File cacheFile = _cacheFile; + String? command; + for (String line in cacheFile.readAsLinesSync()) { + line = line.trim(); + if (line.startsWith(_cacheCommandKey)) { + command = line.substring(line.indexOf('=') + 1).trim(); + break; + } + } + if (command == null) { + printError('Unable to find CMake command in ${cacheFile.path}'); + throw ToolExit(100); + } + return command; + } + + /// Whether or not the project is ready to have CMake commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => _cacheFile.existsSync(); + + /// Runs a `cmake` command with the given parameters. + Future runBuild( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + getCmakeCommand(), + [ + '--build', + buildDirectory.path, + '--target', + target, + if (platform.isWindows) ...['--config', buildMode], + ...arguments, + ], + ); + } +} diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart index e7214bf29714..9da4e89811e6 100644 --- a/script/tool/lib/src/common/gradle.dart +++ b/script/tool/lib/src/common/gradle.dart @@ -14,9 +14,6 @@ const String _gradleWrapperNonWindows = 'gradlew'; class GradleProject { /// Creates an instance that runs commands for [project] with the given /// [processRunner]. - /// - /// If [log] is true, commands run by this instance will long various status - /// messages. GradleProject( this.flutterProject, { this.processRunner = const ProcessRunner(), diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 4911b4aeb156..0b0dd26ba227 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'common/cmake.dart'; import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; @@ -456,8 +457,8 @@ this command. file.basename.endsWith('_tests.exe'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'windows', isTestBinary: isTestBinary); + return _runGoogleTestTests(plugin, 'Windows', 'Debug', + isTestBinary: isTestBinary); } Future<_PlatformResult> _testLinux( @@ -471,8 +472,16 @@ this command. file.basename.endsWith('_tests'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'linux', isTestBinary: isTestBinary); + // Since Linux uses a single-config generator, building-examples only + // generates the build files for release, so the tests have to be run in + // release mode as well. + // + // TODO(stuartmorgan): Consider adding a command to `flutter` that would + // generate build files without doing a build, and using that instead of + // relying on running build-examples. See + // https://github.com/flutter/flutter/issues/93407. + return _runGoogleTestTests(plugin, 'Linux', 'Release', + isTestBinary: isTestBinary); } /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s @@ -482,38 +491,66 @@ this command. /// The binaries are assumed to be Google Test test binaries, thus returning /// zero for success and non-zero for failure. Future<_PlatformResult> _runGoogleTestTests( - RepositoryPackage plugin, { - required String buildDirectoryName, + RepositoryPackage plugin, + String platformName, + String buildMode, { required bool Function(File) isTestBinary, }) async { final List testBinaries = []; + bool hasMissingBuild = false; + bool buildFailed = false; for (final RepositoryPackage example in plugin.getExamples()) { - final Directory buildDir = example.directory - .childDirectory('build') - .childDirectory(buildDirectoryName); - if (!buildDir.existsSync()) { + final CMakeProject project = CMakeProject(example.directory, + buildMode: buildMode, + processRunner: processRunner, + platform: platform); + if (!project.isConfigured()) { + printError('ERROR: Run "flutter build" on ${example.displayName}, ' + 'or run this tool\'s "build-examples" command, for the target ' + 'platform before executing tests.'); + hasMissingBuild = true; continue; } - testBinaries.addAll(buildDir + + // By repository convention, example projects create an aggregate target + // called 'unit_tests' that builds all unit tests (usually just an alias + // for a specific test target). + final int exitCode = await project.runBuild('unit_tests'); + if (exitCode != 0) { + printError('${example.displayName} unit tests failed to build.'); + buildFailed = true; + } + + testBinaries.addAll(project.buildDirectory .listSync(recursive: true) .whereType() .where(isTestBinary) .where((File file) { - // Only run the release build of the unit tests, to avoid running the - // same tests multiple times. Release is used rather than debug since - // `build-examples` builds release versions. + // Only run the `buildMode` build of the unit tests, to avoid running + // the same tests multiple times. final List components = path.split(file.path); - return components.contains('release') || components.contains('Release'); + return components.contains(buildMode) || + components.contains(buildMode.toLowerCase()); })); } + if (hasMissingBuild) { + return _PlatformResult(RunState.failed, + error: 'Examples must be built before testing.'); + } + + if (buildFailed) { + return _PlatformResult(RunState.failed, + error: 'Failed to build $platformName unit tests.'); + } + if (testBinaries.isEmpty) { final String binaryExtension = platform.isWindows ? '.exe' : ''; printError( 'No test binaries found. At least one *_test(s)$binaryExtension ' 'binary should be built by the example(s)'); return _PlatformResult(RunState.failed, - error: 'No $buildDirectoryName unit tests found'); + error: 'No $platformName unit tests found'); } bool passing = true; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index ba93efcb3ace..697cbd4b84d3 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -8,10 +8,12 @@ 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/cmake.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/native_test_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -53,6 +55,16 @@ final Map _kDeviceListMap = { } }; +const String _fakeCmakeCommand = 'path/to/cmake'; + +void _createFakeCMakeCache(Directory pluginDir, Platform platform) { + final CMakeProject project = CMakeProject(pluginDir.childDirectory('example'), + platform: platform, buildMode: 'Release'); + final File cache = project.buildDirectory.childFile('CMakeCache.txt'); + cache.createSync(recursive: true); + cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); +} + // TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of // doing all the process mocking and validation. void main() { @@ -67,7 +79,10 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); + // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing + // needs to distinguish between Linux and macOS, so set both to true to + // allow them to share a setup group. + mockPlatform = MockPlatform(isMacOS: true, isLinux: true); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final NativeTestCommand command = NativeTestCommand(packagesDir, @@ -133,6 +148,26 @@ void main() { package.path); } + // Returns the ProcessCall to expect for build the Linux unit tests for the + // given plugin. + ProcessCall _getLinuxBuildCall(Directory pluginDir) { + return ProcessCall( + 'cmake', + [ + '--build', + pluginDir + .childDirectory('example') + .childDirectory('build') + .childDirectory('linux') + .childDirectory('x64') + .childDirectory('release') + .path, + '--target', + 'unit_tests' + ], + null); + } + test('fails if no platforms are provided', () async { Error? commandError; final List output = await runCapturingPrint( @@ -844,15 +879,16 @@ void main() { }); group('Linux', () { - test('runs unit tests', () async { + test('builds and runs unit tests', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -874,15 +910,16 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); test('only runs release unit tests', () async { const String debugTestBinaryRelativePath = - 'build/linux/foo/debug/bar/plugin_test'; + 'build/linux/x64/debug/bar/plugin_test'; const String releaseTestBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', @@ -890,6 +927,7 @@ void main() { ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File releaseTestBinary = childFileWithSubcomponents( pluginDirectory, @@ -909,15 +947,15 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(releaseTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), @@ -936,22 +974,56 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getLinuxBuildCall(pluginDirectory), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; + 'build/linux/x64/release/bar/plugin_test'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -979,6 +1051,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1524,16 +1597,37 @@ void main() { runner.addCommand(command); }); + // Returns the ProcessCall to expect for build the Windows unit tests for + // the given plugin. + ProcessCall _getWindowsBuildCall(Directory pluginDir) { + return ProcessCall( + _fakeCmakeCommand, + [ + '--build', + pluginDir + .childDirectory('example') + .childDirectory('build') + .childDirectory('windows') + .path, + '--target', + 'unit_tests', + '--config', + 'Debug' + ], + null); + } + group('Windows', () { test('runs unit tests', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1555,15 +1649,16 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getWindowsBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); - test('only runs release unit tests', () async { + test('only runs debug unit tests', () async { const String debugTestBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Release/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', @@ -1571,10 +1666,10 @@ void main() { ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); - final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, - ['example', ...releaseTestBinaryRelativePath.split('/')]); + final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...debugTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ 'native-test', @@ -1590,15 +1685,15 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(releaseTestBinary.path, const [], null), + _getWindowsBuildCall(pluginDirectory), + ProcessCall(debugTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), @@ -1617,22 +1712,56 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getWindowsBuildCall(pluginDirectory), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(pluginDirectory, mockPlatform); final File testBinary = childFileWithSubcomponents(pluginDirectory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1660,6 +1789,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getWindowsBuildCall(pluginDirectory), ProcessCall(testBinary.path, const [], null), ])); }); From 45f87b92680110bfb78c88019c80bf0f954e465a Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 15 Nov 2021 18:46:04 +0100 Subject: [PATCH 166/259] [in_app_purchase] Add support for promotional offers through Store-Kit wrappers (#4458) --- .../RunnerTests/InAppPurchasePluginTests.m | 92 +++++++++++- .../example/ios/RunnerTests/Stubs.m | 8 + .../example/ios/RunnerTests/TranslatorTests.m | 141 ++++++++++++++++++ .../ios/Classes/FIAObjectTranslator.h | 9 ++ .../ios/Classes/FIAObjectTranslator.m | 74 +++++++++ .../ios/Classes/InAppPurchasePlugin.m | 19 +++ .../sk_payment_queue_wrapper.dart | 122 ++++++++++++++- .../sk_payment_queue_wrapper.g.dart | 19 +++ .../sk_product_wrapper.dart | 17 ++- .../sk_product_wrapper.g.dart | 5 + .../store_kit_wrappers/sk_product_test.dart | 1 + .../sk_test_stub_objects.dart | 2 + 12 files changed, 500 insertions(+), 9 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index b51f622e939b..84e6f7197f40 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -111,7 +111,7 @@ - (void)testAddPaymentFailure { XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } -- (void)testAddPaymentSuccessWithMockQueue { +- (void)testAddPaymentSuccessWithoutPaymentDiscount { XCTestExpectation* expectation = [self expectationWithDescription:@"result should return success state"]; FlutterMethodCall* call = @@ -129,6 +129,9 @@ - (void)testAddPaymentSuccessWithMockQueue { SKPaymentTransaction* transaction = transactions[0]; if (transaction.transactionState == SKPaymentTransactionStatePurchased) { transactionForUpdateBlock = transaction; + if (@available(iOS 12.2, *)) { + XCTAssertNil(transaction.payment.paymentDiscount); + } [expectation fulfill]; } } @@ -147,6 +150,93 @@ - (void)testAddPaymentSuccessWithMockQueue { XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } +- (void)testAddPaymentSuccessWithPaymentDiscount { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + @"paymentDiscount" : @{ + @"identifier" : @"test_identifier", + @"keyIdentifier" : @"test_key_identifier", + @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + @"signature" : @"test_signature", + @"timestamp" : @(1635847102), + } + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + if (@available(iOS 12.2, *)) { + SKPaymentDiscount* paymentDiscount = transaction.payment.paymentDiscount; + XCTAssertEqual(paymentDiscount.identifier, @"test_identifier"); + XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier"); + XCTAssertEqualObjects( + paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]); + XCTAssertEqual(paymentDiscount.signature, @"test_signature"); + XCTAssertEqual(paymentDiscount.timestamp, @(1635847102)); + } + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testAddPaymentFailureWithInvalidPaymentDiscount { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + @"paymentDiscount" : @{ + @"keyIdentifier" : @"test_key_identifier", + @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + @"signature" : @"test_signature", + @"timestamp" : @(1635847102), + } + }]; + + [self.plugin + handleMethodCall:call + result:^(id r) { + XCTAssertTrue([r isKindOfClass:FlutterError.class]); + FlutterError* result = r; + XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object"); + XCTAssertEqualObjects(result.message, + @"You have requested a payment and specified a payment " + @"discount with invalid properties. When specifying a " + @"payment discount the 'identifier' field is mandatory."); + XCTAssertEqualObjects(result.details, call.arguments); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; +} + - (void)testAddPaymentWithNullSandboxArgument { XCTestExpectation* expectation = [self expectationWithDescription:@"result should return success state"]; diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 364505d6754a..e4277d3edd59 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -61,6 +61,14 @@ - (instancetype)initWithMap:(NSDictionary *)map { [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] forKey:@"subscriptionGroupIdentifier"]; } + if (@available(iOS 12.2, *)) { + NSMutableArray *discounts = [[NSMutableArray alloc] init]; + for (NSDictionary *discountMap in map[@"discounts"]) { + [discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]]; + } + + [self setValue:discounts forKey:@"discounts"]; + } } return self; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m index 89a7b2c84380..e014419d76fb 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m @@ -14,6 +14,7 @@ @interface TranslatorTest : XCTestCase @property(strong, nonatomic) NSMutableDictionary *productMap; @property(strong, nonatomic) NSDictionary *productResponseMap; @property(strong, nonatomic) NSDictionary *paymentMap; +@property(copy, nonatomic) NSDictionary *paymentDiscountMap; @property(strong, nonatomic) NSDictionary *transactionMap; @property(strong, nonatomic) NSDictionary *errorMap; @property(strong, nonatomic) NSDictionary *localeMap; @@ -45,6 +46,9 @@ - (void)setUp { self.productMap[@"subscriptionPeriod"] = self.periodMap; self.productMap[@"introductoryPrice"] = self.discountMap; } + if (@available(iOS 12.2, *)) { + self.productMap[@"discounts"] = @[ self.discountMap ]; + } if (@available(iOS 12.0, *)) { self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; @@ -59,6 +63,13 @@ - (void)setUp { @"applicationUsername" : @"app user name", @"simulatesAskToBuyInSandbox" : @(NO) }; + self.paymentDiscountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; NSDictionary *originalTransactionMap = @{ @"transactionIdentifier" : @"567", @"transactionState" : @(SKPaymentTransactionStatePurchasing), @@ -175,4 +186,134 @@ - (void)testSKStorefrontAndSKPaymentTransactionToMap { } } +- (void)testSKPaymentDiscountFromMap { + if (@available(iOS 12.2, *)) { + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error]; + + XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]); + } +} + +- (void)testSKPaymentDiscountFromMapMissingIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : value, + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'identifier' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : value, + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingNonce { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : value, + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects(error, + @"When specifying a payment discount the 'nonce' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingSignature { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : value, + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'signature' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingTimestamp { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : value, + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'timestamp' field is mandatory."); + } + } +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h index 95a5edc245dc..eb97ceb44754 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -20,6 +20,10 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount API_AVAILABLE(ios(11.2)); +// Converts an array of SKProductDiscount instances into an array of dictionaries. ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts API_AVAILABLE(ios(12.2)); + // Converts an instance of SKProductsResponse into a dictionary. + (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; @@ -47,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN andSKPaymentTransaction:(SKPaymentTransaction *)transaction API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); +// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary. ++ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString *_Nullable *_Nullable)error + API_AVAILABLE(ios(12.2)); + @end ; diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m index 0125604b3b3c..3ceb512abb10 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -35,6 +35,10 @@ + (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { ?: [NSNull null] forKey:@"introductoryPrice"]; } + if (@available(iOS 12.2, *)) { + [map setObject:[FIAObjectTranslator getMapArrayFromSKProductDiscounts:product.discounts] + forKey:@"discounts"]; + } if (@available(iOS 12.0, *)) { [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] forKey:@"subscriptionGroupIdentifier"]; @@ -49,6 +53,17 @@ + (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPe return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; } ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts { + NSMutableArray *discountsMapArray = [[NSMutableArray alloc] init]; + + for (SKProductDiscount *productDiscount in productDiscounts) { + [discountsMapArray addObject:[FIAObjectTranslator getMapFromSKProductDiscount:productDiscount]]; + } + + return discountsMapArray; +} + + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { if (!discount) { return nil; @@ -193,4 +208,63 @@ + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront return map; } ++ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString **)error { + if (!map || map.count <= 0) { + return nil; + } + + NSString *identifier = map[@"identifier"]; + NSString *keyIdentifier = map[@"keyIdentifier"]; + NSString *nonce = map[@"nonce"]; + NSString *signature = map[@"signature"]; + NSNumber *timestamp = map[@"timestamp"]; + + if (!identifier || ![identifier isKindOfClass:NSString.class] || + [identifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'identifier' field is mandatory."; + } + return nil; + } + + if (!keyIdentifier || ![keyIdentifier isKindOfClass:NSString.class] || + [keyIdentifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'keyIdentifier' field is mandatory."; + } + return nil; + } + + if (!nonce || ![nonce isKindOfClass:NSString.class] || [nonce isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'nonce' field is mandatory."; + } + return nil; + } + + if (!signature || ![signature isKindOfClass:NSString.class] || [signature isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'signature' field is mandatory."; + } + return nil; + } + + if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) { + if (error) { + *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; + } + return nil; + } + + SKPaymentDiscount *discount = + [[SKPaymentDiscount alloc] initWithIdentifier:identifier + keyIdentifier:keyIdentifier + nonce:[[NSUUID alloc] initWithUUIDString:nonce] + signature:signature + timestamp:timestamp]; + + return discount; +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index 7e2d2ca80675..661f57f432d8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -203,6 +203,25 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { ? NO : [simulatesAskToBuyInSandbox boolValue]; + if (@available(iOS 12.2, *)) { + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator + getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"] + withError:&error]; + + if (error) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_discount_object" + message:[NSString stringWithFormat:@"You have requested a payment and specified a " + @"payment discount with invalid properties. %@", + error] + details:call.arguments]); + return; + } + + payment.paymentDiscount = paymentDiscount; + } + if (![self.paymentQueueHandler addPayment:payment]) { result([FlutterError errorWithCode:@"storekit_duplicate_product_object" diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 3decba2a9818..918eac05822a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -378,12 +378,14 @@ class SKError { @JsonSerializable(createToJson: true) class SKPaymentWrapper { /// Creates a new [SKPaymentWrapper] with the provided information. - const SKPaymentWrapper( - {required this.productIdentifier, - this.applicationUsername, - this.requestData, - this.quantity = 1, - this.simulatesAskToBuyInSandbox = false}); + const SKPaymentWrapper({ + required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false, + this.paymentDiscount, + }); /// Constructs an instance of this from a key value map of data. /// @@ -450,6 +452,14 @@ class SKPaymentWrapper { /// testing. final bool simulatesAskToBuyInSandbox; + /// The details of a discount that should be applied to the payment. + /// + /// See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc) + /// for more information on generating keys and creating offers for + /// auto-renewable subscriptions. If set to `null` no discount will be + /// applied to this payment. + final SKPaymentDiscountWrapper? paymentDiscount; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -473,3 +483,103 @@ class SKPaymentWrapper { @override String toString() => _$SKPaymentWrapperToJson(this).toString(); } + +/// Dart wrapper around StoreKit's +/// [SKPaymentDiscount](https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc). +/// +/// Used to indicate a discount is applicable to a payment. The +/// [SKPaymentDiscountWrapper] instance should be assigned to the +/// [SKPaymentWrapper] object to which the discount should be applied. +/// Discount offers are set up in App Store Connect. See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc) +/// for more information. +@immutable +@JsonSerializable(createToJson: true) +class SKPaymentDiscountWrapper { + /// Creates a new [SKPaymentDiscountWrapper] with the provided information. + const SKPaymentDiscountWrapper({ + required this.identifier, + required this.keyIdentifier, + required this.nonce, + required this.signature, + required this.timestamp, + }); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory SKPaymentDiscountWrapper.fromJson(Map map) { + assert(map != null); + return _$SKPaymentDiscountWrapperFromJson(map); + } + + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'identifier': identifier, + 'keyIdentifier': keyIdentifier, + 'nonce': nonce, + 'signature': signature, + 'timestamp': timestamp, + }; + } + + /// The identifier of the discount offer. + /// + /// The identifier must match one of the offers set up in App Store Connect. + final String identifier; + + /// A string identifying the key that is used to generate the signature. + /// + /// Keys are generated and downloaded from App Store Connect. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String keyIdentifier; + + /// A universal unique identifier (UUID) created together with the signature. + /// + /// The UUID should be generated on your server when it creates the + /// `signature` for the payment discount. The UUID can be used once, a new + /// UUID should be created for each payment request. The string representation + /// of the UUID must be lowercase. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String nonce; + + /// A cryptographically signed string representing the to properties of the + /// promotional offer. + /// + /// The signature is string signed with a private key and contains all the + /// properties of the promotional offer. To keep you private key secure the + /// signature should be created on a server. See [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String signature; + + /// The date and time the signature was created. + /// + /// The timestamp should be formatted in Unix epoch time. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final int timestamp; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKPaymentDiscountWrapper typedOther = + other as SKPaymentDiscountWrapper; + return typedOther.identifier == identifier && + typedOther.keyIdentifier == keyIdentifier && + typedOther.nonce == nonce && + typedOther.signature == signature && + typedOther.timestamp == timestamp; + } + + @override + int get hashCode => + hashValues(identifier, keyIdentifier, nonce, signature, timestamp); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 8c0e64b2b9f6..f594ad450440 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -32,3 +32,22 @@ Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => 'quantity': instance.quantity, 'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox, }; + +SKPaymentDiscountWrapper _$SKPaymentDiscountWrapperFromJson(Map json) => + SKPaymentDiscountWrapper( + identifier: json['identifier'] as String, + keyIdentifier: json['keyIdentifier'] as String, + nonce: json['nonce'] as String, + signature: json['signature'] as String, + timestamp: json['timestamp'] as int, + ); + +Map _$SKPaymentDiscountWrapperToJson( + SKPaymentDiscountWrapper instance) => + { + 'identifier': instance.identifier, + 'keyIdentifier': instance.keyIdentifier, + 'nonce': instance.nonce, + 'signature': instance.signature, + 'timestamp': instance.timestamp, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 1b681f24f8db..4110754da57b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -241,6 +241,7 @@ class SKProductWrapper { required this.price, this.subscriptionPeriod, this.introductoryPrice, + this.discounts = const [], }); /// Constructing an instance from a map from the Objective-C layer. @@ -295,6 +296,16 @@ class SKProductWrapper { /// and their units and duration do not have to be matched. final SKProductDiscountWrapper? introductoryPrice; + /// An array of subscription offers available for the auto-renewable subscription (available on iOS 12.2 and higher). + /// + /// This property lists all promotional offers set up in App Store Connect. If + /// no promotional offers have been set up, this field returns an empty list. + /// Each [subscriptionPeriod] of individual discounts are independent of the + /// product's [subscriptionPeriod] and their units and duration do not have to + /// be matched. + @JsonKey(defaultValue: []) + final List discounts; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -311,7 +322,8 @@ class SKProductWrapper { typedOther.subscriptionGroupIdentifier == subscriptionGroupIdentifier && typedOther.price == price && typedOther.subscriptionPeriod == subscriptionPeriod && - typedOther.introductoryPrice == introductoryPrice; + typedOther.introductoryPrice == introductoryPrice && + DeepCollectionEquality().equals(typedOther.discounts, discounts); } @override @@ -323,7 +335,8 @@ class SKProductWrapper { this.subscriptionGroupIdentifier, this.price, this.subscriptionPeriod, - this.introductoryPrice); + this.introductoryPrice, + this.discounts); } /// Object that indicates the locale of the price diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index c9079ff64a1e..6eea3ff34da0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -65,6 +65,11 @@ SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( ? null : SKProductDiscountWrapper.fromJson( Map.from(json['introductoryPrice'] as Map)), + discounts: (json['discounts'] as List?) + ?.map((e) => SKProductDiscountWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], ); SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) => diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart index 6a33b75d9808..4d010aad132c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart @@ -84,6 +84,7 @@ void main() { expect(wrapper.subscriptionGroupIdentifier, null); expect(wrapper.price, ''); expect(wrapper.subscriptionPeriod, null); + expect(wrapper.discounts, []); }); test('toProductDetails() should return correct Product object', () { diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart index 595a074f1cfe..a2cf2a0cccfc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -68,6 +68,7 @@ final SKProductWrapper dummyProductWrapper = SKProductWrapper( price: '1.0', subscriptionPeriod: dummySubscription, introductoryPrice: dummyDiscount, + discounts: [dummyDiscount], ); final SkProductResponseWrapper dummyProductResponseWrapper = @@ -118,6 +119,7 @@ Map buildProductMap(SKProductWrapper product) { 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), + 'discounts': [buildDiscountMap(product.introductoryPrice!)], }; } From 184d3a6f8575e2c2daaf009a0f51368222ab066b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 15 Nov 2021 15:31:07 -0500 Subject: [PATCH 167/259] [path_provider] Fix links in READMEs (#4504) --- packages/path_provider/path_provider_android/CHANGELOG.md | 4 ++++ packages/path_provider/path_provider_android/README.md | 2 +- packages/path_provider/path_provider_android/pubspec.yaml | 2 +- packages/path_provider/path_provider_ios/CHANGELOG.md | 4 ++++ packages/path_provider/path_provider_ios/README.md | 2 +- packages/path_provider/path_provider_ios/pubspec.yaml | 2 +- packages/path_provider/path_provider_linux/CHANGELOG.md | 6 +++++- packages/path_provider/path_provider_linux/README.md | 2 +- packages/path_provider/path_provider_linux/pubspec.yaml | 2 +- packages/path_provider/path_provider_macos/CHANGELOG.md | 8 ++++++-- packages/path_provider/path_provider_macos/README.md | 2 +- packages/path_provider/path_provider_macos/pubspec.yaml | 2 +- 12 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index dc24db654c90..eb155d138218 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.7 + +* Fixes link in README. + ## 2.0.6 * Split from `path_provider` as a federated implementation. diff --git a/packages/path_provider/path_provider_android/README.md b/packages/path_provider/path_provider_android/README.md index 7f2603d30e06..b425b5eb5a9a 100644 --- a/packages/path_provider/path_provider_android/README.md +++ b/packages/path_provider/path_provider_android/README.md @@ -1,6 +1,6 @@ # path\_provider\_android -The Android implementation of [`path_provider`]. +The Android implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 3d625c08a9a6..2fd72efa195d 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_android description: Android implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/master/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.6 +version: 2.0.7 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md index dc24db654c90..eb155d138218 100644 --- a/packages/path_provider/path_provider_ios/CHANGELOG.md +++ b/packages/path_provider/path_provider_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.7 + +* Fixes link in README. + ## 2.0.6 * Split from `path_provider` as a federated implementation. diff --git a/packages/path_provider/path_provider_ios/README.md b/packages/path_provider/path_provider_ios/README.md index b170d1762f41..dcfa06dcdfb0 100644 --- a/packages/path_provider/path_provider_ios/README.md +++ b/packages/path_provider/path_provider_ios/README.md @@ -1,6 +1,6 @@ # path\_provider\_ios -The iOS implementation of [`path_provider`]. +The iOS implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_ios/pubspec.yaml b/packages/path_provider/path_provider_ios/pubspec.yaml index d6d2bf29d9b2..9818cbfcd391 100644 --- a/packages/path_provider/path_provider_ios/pubspec.yaml +++ b/packages/path_provider/path_provider_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_ios description: iOS implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.6 +version: 2.0.7 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index a3febb0d108b..209fddbd61bd 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.2 + +* Fixes link in README. + ## 2.1.1 * Removed obsolete `pluginClass: none` from pubpsec. @@ -46,5 +50,5 @@ * This updates the readme and pubspec and example to reflect the endorsement of this implementation of `path_provider` ## 0.0.1 -* The initial implementation of path_provider for Linux +* The initial implementation of path\_provider for Linux * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md index b0b73dcb0ecd..281873bdade1 100644 --- a/packages/path_provider/path_provider_linux/README.md +++ b/packages/path_provider/path_provider_linux/README.md @@ -1,6 +1,6 @@ # path\_provider\_linux -The linux implementation of [`path_provider`]. +The linux implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index bc1788729832..67638cdef332 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index 1d0738c3757a..d88ed065abb1 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,4 +1,8 @@ -# 2.0.2 +## 2.0.3 + +* Fixes link in README. + +## 2.0.2 * Add Swift language version to podspec. * Add native unit tests. @@ -55,7 +59,7 @@ ## 0.0.3+1 -* Make the pedantic dev_dependency explicit. +* Make the pedantic `dev_dependency` explicit. ## 0.0.3 diff --git a/packages/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_macos/README.md index 00abdf24cd79..6641134aefd9 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -1,6 +1,6 @@ # path\_provider\_macos -The macos implementation of [`path_provider`]. +The macos implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 140e4cde9d58..ac6011d33d6d 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_macos description: macOS implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" From b6e8d056ed7781c1dae6388a79770e2d87fcbb19 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 15 Nov 2021 15:31:22 -0500 Subject: [PATCH 168/259] [camera] Fix version and CHANGELOG (#4513) https://github.com/flutter/plugins/pull/4423 incorrectly added changelog entries to an already-published version instead of bumping the version, whcih was missed during review. Fixes https://github.com/flutter/flutter/issues/93659 --- packages/camera/camera/CHANGELOG.md | 5 ++++- packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 29750f9cefff..929feeac6b69 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,8 +1,11 @@ -## 0.9.4+3 +## 0.9.4+4 * Change Android compileSdkVersion to 31. * Remove usages of deprecated Android API `CamcorderProfile`. * Update gradle version to 7.0.2 on Android. + +## 0.9.4+3 + * Fix registerTexture and result being called on background thread on iOS. ## 0.9.4+2 diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 6b4db7cd91ec..905ec4392dd7 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/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+3 +version: 0.9.4+4 environment: sdk: ">=2.14.0 <3.0.0" From b8185a3aa76cb092a12b16aa4a67b88f20f10c70 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Mon, 15 Nov 2021 13:21:04 -0800 Subject: [PATCH 169/259] [url_launcher] Add null check for extracting browser headers (#4507) --- packages/url_launcher/url_launcher/CHANGELOG.md | 4 ++++ .../plugins/urllauncher/WebViewActivity.java | 9 ++++++++- .../urllauncher/WebViewActivityTest.java | 17 +++++++++++++++++ packages/url_launcher/url_launcher/pubspec.yaml | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index fff325e08915..5aa0154dafd1 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.13 + +* Fixed extracting browser headers when they are null error. + ## 6.0.12 * Fixed an error where 'launch' method of url_launcher would cause an error if the provided URL was not valid by RFC 3986. diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java index 7f26c18740ec..ec8cde514621 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java +++ b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java @@ -20,7 +20,10 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -142,7 +145,11 @@ public void onCreate(Bundle savedInstanceState) { registerReceiver(broadcastReceiver, closeIntentFilter); } - private Map extractHeaders(Bundle headersBundle) { + @VisibleForTesting + public static Map extractHeaders(@Nullable Bundle headersBundle) { + if (headersBundle == null) { + return Collections.emptyMap(); + } final Map headersMap = new HashMap<>(); for (String key : headersBundle.keySet()) { final String value = headersBundle.getString(key); diff --git a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java new file mode 100644 index 000000000000..d0b0508d2307 --- /dev/null +++ b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.urllauncher; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import org.junit.Test; + +public class WebViewActivityTest { + @Test + public void extractHeaders_returnsEmptyMapWhenHeadersBundleNull() { + assertEquals(WebViewActivity.extractHeaders(null), Collections.emptyMap()); + } +} diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 8edb9e21c535..e057115e65d4 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/master/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.12 +version: 6.0.13 environment: sdk: ">=2.14.0 <3.0.0" From 1e85f320695dbc6676602dee99e8a9acc89ad4ee Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 15 Nov 2021 17:06:08 -0500 Subject: [PATCH 170/259] Add link to CHANGELOG style in PR template (#4512) --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 47d4d48adf7d..e19d8e1d80cd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ - [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. `[shared_preferences]` - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. -- [ ] I updated `CHANGELOG.md` to add a description of the change. +- [ ] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. @@ -30,5 +30,6 @@ If you need help, consider asking for advice on the #hackers-new channel on [Dis [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates +[following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style [the auto-formatter]: https://github.com/flutter/plugins/blob/master/script/tool/README.md#format-code [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests From e6ff3527c3c103cfd4c5f06b11d4ad331e0af627 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 16 Nov 2021 15:47:33 -0500 Subject: [PATCH 171/259] [webview_flutter] Fix Flaky Resize Test (#4460) --- .../webview_flutter_test.dart | 179 ++++++++++-------- .../webview_flutter_test.dart | 177 +++++++++-------- .../webview_flutter_test.dart | 173 +++++++++-------- 3 files changed, 277 insertions(+), 252 deletions(-) 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 69ddcc685ff8..0baffba0edb7 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 @@ -8,6 +8,7 @@ import 'dart:io'; 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'; @@ -180,91 +181,30 @@ void main() { }, skip: Platform.isAndroid && _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + if (Platform.isAndroid) { + await initialResizeCompleter.future; + } + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -1496,3 +1436,74 @@ Future _runJavascriptReturningResult( } return jsonDecode(await controller.runJavascriptReturningResult(js)); } + +class ResizableWebView extends StatefulWidget { + ResizableWebView({required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: Text('ResizeButton'), + ), + ], + ), + ); + } +} 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 8e3dcb458ee8..454e2c959680 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 @@ -185,91 +185,29 @@ void main() { }, skip: _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + print(resizeButtonTapped); + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + await initialResizeCompleter.future; + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -1440,3 +1378,74 @@ Future _runJavaScriptReturningResult( WebViewController controller, String js) async { return jsonDecode(await controller.runJavascriptReturningResult(js)); } + +class ResizableWebView extends StatefulWidget { + ResizableWebView({required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: Text('ResizeButton'), + ), + ], + ), + ); + } +} 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 17e896c94888..a637da13ddc0 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 @@ -8,6 +8,7 @@ import 'dart:io'; 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'; @@ -180,91 +181,24 @@ void main() { }); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -1235,3 +1169,74 @@ Future _runJavascriptReturningResult( } return jsonDecode(await controller.runJavascriptReturningResult(js)); } + +class ResizableWebView extends StatefulWidget { + ResizableWebView({required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: Text('ResizeButton'), + ), + ], + ), + ); + } +} From 2aa2e5a8dfc2e02c1aaf6a23af85f1c07a3301cf Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Wed, 17 Nov 2021 13:38:02 +0100 Subject: [PATCH 172/259] [in_app_purchase]Iap/ios add cancel status (#4094) --- .../in_app_purchase_ios/CHANGELOG.md | 5 ++ .../store_kit_wrappers/enum_converters.dart | 11 +++- .../src/types/app_store_purchase_details.dart | 5 +- .../in_app_purchase_ios/pubspec.yaml | 4 +- .../test/fakes/fake_ios_platform.dart | 23 +++++++- .../in_app_purchase_ios_platform_test.dart | 58 ++++++++++++++++++- 6 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 4daa6a2d1dcb..b1e40be882c0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.0 + +* BREAKING CHANGE : Refactor to handle new `PurchaseStatus` named `canceled`. This means developers + can distinguish between an error and user cancellation. + ## 0.1.4 * Require Dart SDK >= 2.14. diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index 8a0526f07ad7..1c2bee5a069a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -30,7 +30,8 @@ class SKTransactionStatusConverter } /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. - PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { + PurchaseStatus toPurchaseStatus( + SKPaymentTransactionStateWrapper object, SKError? error) { switch (object) { case SKPaymentTransactionStateWrapper.purchasing: case SKPaymentTransactionStateWrapper.deferred: @@ -40,6 +41,14 @@ class SKTransactionStatusConverter case SKPaymentTransactionStateWrapper.restored: return PurchaseStatus.restored; case SKPaymentTransactionStateWrapper.failed: + // According to the Apple documentation the error code "2" indicates + // the user cancelled the payment (SKErrorPaymentCancelled) and error + // code "15" indicates the cancellation of the overlay (SKErrorOverlayCancelled). + // An overview of all error codes can be found at: https://developer.apple.com/documentation/storekit/skerrorcode?language=objc + if (error != null && (error.code == 2 || error.code == 15)) { + return PurchaseStatus.canceled; + } + return PurchaseStatus.error; case SKPaymentTransactionStateWrapper.unspecified: return PurchaseStatus.error; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart index 6d6f241d6ca8..58e965a1f19f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart @@ -56,7 +56,7 @@ class AppStorePurchaseDetails extends PurchaseDetails { purchaseID: transaction.transactionIdentifier, skPaymentTransaction: transaction, status: SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState), + .toPurchaseStatus(transaction.transactionState, transaction.error), transactionDate: transaction.transactionTimeStamp != null ? (transaction.transactionTimeStamp! * 1000).toInt().toString() : null, @@ -66,7 +66,8 @@ class AppStorePurchaseDetails extends PurchaseDetails { source: kIAPSource), ); - if (purchaseDetails.status == PurchaseStatus.error) { + if (purchaseDetails.status == PurchaseStatus.error || + purchaseDetails.status == PurchaseStatus.canceled) { purchaseDetails.error = IAPError( source: kIAPSource, code: kPurchaseErrorCode, diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 80ef921cada5..5129f5ebd66c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4 +version: 0.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - in_app_purchase_platform_interface: ^1.1.0 + in_app_purchase_platform_interface: ^1.3.0 json_annotation: ^4.3.0 meta: ^1.3.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart index e7dbd1a49ae2..30a42b249001 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -18,7 +18,7 @@ class FakeIOSPlatform { channel.setMockMethodCallHandler(onMethodCall); } - // pre-configured store informations + // pre-configured store information String? receiptData; late Set validProductIDs; late Map validProducts; @@ -26,6 +26,7 @@ class FakeIOSPlatform { late List finishedTransactions; late bool testRestoredTransactionsNull; late bool testTransactionFail; + late int testTransactionCancel; PlatformException? queryProductException; PlatformException? restoreException; SKError? testRestoredError; @@ -67,6 +68,7 @@ class FakeIOSPlatform { finishedTransactions = []; testRestoredTransactionsNull = false; testTransactionFail = false; + testTransactionCancel = -1; queryProductException = null; restoreException = null; testRestoredError = null; @@ -107,6 +109,20 @@ class FakeIOSPlatform { originalTransaction: null); } + SKPaymentTransactionWrapper createCanceledTransaction( + String productId, int errorCode) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionTimeStamp: 123123.121, + error: SKError( + code: errorCode, + domain: 'ios_domain', + userInfo: {'message': 'an error message'}), + originalTransaction: null); + } + Future onMethodCall(MethodCall call) { switch (call.method) { case '-[SKPaymentQueue canMakePayments:]': @@ -167,6 +183,11 @@ class FakeIOSPlatform { createFailedTransaction(id); InAppPurchaseIosPlatform.observer .updatedTransactions(transactions: [transaction_failed]); + } else if (testTransactionCancel > 0) { + SKPaymentTransactionWrapper transaction_canceled = + createCanceledTransaction(id, testTransactionCancel); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_canceled]); } else { SKPaymentTransactionWrapper transaction_finished = createPurchasedTransaction( diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index 865468f532bf..76d30af125e4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -130,7 +130,7 @@ void main() { expect( actual.status, SKTransactionStatusConverter() - .toPurchaseStatus(expected.transactionState), + .toPurchaseStatus(expected.transactionState, expected.error), ); expect(actual.verificationData.localVerificationData, fakeIOSPlatform.receiptData); @@ -275,6 +275,62 @@ void main() { expect(completerError.message, 'ios_domain'); expect(completerError.details, {'message': 'an error message'}); }); + + test( + 'should get canceled purchase status when error code is SKErrorPaymentCancelled', + () async { + fakeIOSPlatform.testTransactionCancel = 2; + List details = []; + Completer completer = Completer(); + + Stream> stream = iapIosPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.status == PurchaseStatus.canceled) { + completer.complete(purchaseDetails.status); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + PurchaseStatus purchaseStatus = await completer.future; + expect(purchaseStatus, PurchaseStatus.canceled); + }); + + test( + 'should get canceled purchase status when error code is SKErrorOverlayCancelled', + () async { + fakeIOSPlatform.testTransactionCancel = 15; + List details = []; + Completer completer = Completer(); + + Stream> stream = iapIosPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.status == PurchaseStatus.canceled) { + completer.complete(purchaseDetails.status); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + PurchaseStatus purchaseStatus = await completer.future; + expect(purchaseStatus, PurchaseStatus.canceled); + }); }); group('complete purchase', () { From 4ccc90493235419ef01337a19a7013bee578864a Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Wed, 17 Nov 2021 15:28:08 +0100 Subject: [PATCH 173/259] [in_app_purchase]IAP/android add cancel status (#4095) --- .../in_app_purchase_android/CHANGELOG.md | 5 ++ .../src/in_app_purchase_android_platform.dart | 12 +++- .../in_app_purchase_android/pubspec.yaml | 4 +- ...in_app_purchase_android_platform_test.dart | 68 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) 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 c504bd76cdc5..78aa8db43bcc 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,8 @@ +## 0.2.0 + +* BREAKING CHANGE : Refactor to handle new `PurchaseStatus` named `canceled`. This means developers + can distinguish between an error and user cancellation. + ## 0.1.6 * Require Dart SDK >= 2.14. 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 f71132a77ef3..7deec6e6ab69 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 @@ -260,8 +260,12 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final List> purchases = resultWrapper.purchasesList.map((PurchaseWrapper purchase) { - return _maybeAutoConsumePurchase( - GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error); + final GooglePlayPurchaseDetails googlePlayPurchaseDetails = + GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; + if (resultWrapper.responseCode == BillingResponse.userCanceled) { + googlePlayPurchaseDetails.status = PurchaseStatus.canceled; + } + return _maybeAutoConsumePurchase(googlePlayPurchaseDetails); }).toList(); if (purchases.isNotEmpty) { return Future.wait(purchases); @@ -270,7 +274,9 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { PurchaseDetails( purchaseID: '', productID: '', - status: PurchaseStatus.error, + status: resultWrapper.responseCode == BillingResponse.userCanceled + ? PurchaseStatus.canceled + : PurchaseStatus.error, transactionDate: null, verificationData: PurchaseVerificationData( localVerificationData: '', 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 fef2dab2ff74..7d5c610aca14 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.6 +version: 0.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -20,7 +20,7 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - in_app_purchase_platform_interface: ^1.1.0 + in_app_purchase_platform_interface: ^1.3.0 json_annotation: ^4.3.0 meta: ^1.3.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index bd704894daf8..12cca69793ac 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -616,6 +616,74 @@ void main() { purchaseParam: purchaseParam, autoConsume: false); expect(null, await consumeCompleter.future); }); + + test( + 'should get canceled purchase status when response code is BillingResponse.userCanceled', + () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.userCanceled; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'sku': skuDetails.sku, + 'isAutoRenewing': false, + 'packageName': "package", + 'purchaseTime': 1231231231, + 'purchaseToken': "token", + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + iapAndroidPlatform.billingClient.callHandler(call); + }); + Completer consumeCompleter = Completer(); + // adding call back for consume purchase + final BillingResponse expectedCode = BillingResponse.userCanceled; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResultForConsume), + additionalStepBeforeReturn: (dynamic args) { + String purchaseToken = args['purchaseToken']; + consumeCompleter.complete(purchaseToken); + }); + + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + Stream purchaseStream = iapAndroidPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = purchaseStream.listen((_) { + purchaseDetails = _.first; + completer.complete(purchaseDetails); + subscription.cancel(); + }, onDone: () {}); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + applicationUserName: accountId); + await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); + + // Verify that the result has an error for the failed consumption + GooglePlayPurchaseDetails result = await completer.future; + expect(result.status, PurchaseStatus.canceled); + }); }); group('complete purchase', () { From 8f55fb68d0e91f35dd5b01c58f4f977ade2cba66 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 17 Nov 2021 13:37:46 -0500 Subject: [PATCH 174/259] [webview_flutter_android] Implementation of Android WebView widget using pigeon (#4503) --- .../webview_flutter/analysis_options.yaml | 1 - .../webview_flutter/analysis_options.yaml | 1 + .../webview_flutter_test.dart | 8 +- .../webview_flutter_android/CHANGELOG.md | 4 + .../FlutterDownloadListener.java | 33 - .../webviewflutter/FlutterWebView.java | 495 -------------- .../webviewflutter/FlutterWebViewClient.java | 323 --------- .../webviewflutter/FlutterWebViewFactory.java | 23 +- .../webviewflutter/JavaScriptChannel.java | 49 +- .../JavaScriptChannelHostApiImpl.java | 12 +- .../WebChromeClientHostApiImpl.java | 43 +- .../webviewflutter/WebViewBuilder.java | 172 ----- .../webviewflutter/WebViewFlutterPlugin.java | 130 +++- .../webviewflutter/WebViewHostApiImpl.java | 15 +- .../FlutterDownloadListenerTest.java | 42 -- .../FlutterWebViewClientTest.java | 60 -- .../webviewflutter/FlutterWebViewTest.java | 173 ----- .../webviewflutter/WebChromeClientTest.java | 48 ++ .../webviewflutter/WebViewBuilderTest.java | 104 --- .../webview_flutter_test.dart | 61 +- .../example/lib/main.dart | 52 +- .../example/lib/navigation_request.dart | 2 +- .../example/lib/web_view.dart | 32 +- .../example/pubspec.yaml | 4 +- .../lib/src/android_webview.dart | 12 +- .../lib/src/android_webview_api_impls.dart | 141 ++-- .../lib/src/instance_manager.dart | 4 +- .../lib/webview_android.dart | 65 +- .../lib/webview_android_widget.dart | 620 ++++++++++++++++++ .../lib/webview_surface_android.dart | 81 +-- .../webview_flutter_android/pubspec.yaml | 4 +- .../test/android_webview.pigeon.dart | 134 ++-- .../test/android_webview_test.dart | 2 +- .../test/webview_android_widget_test.dart | 565 ++++++++++++++++ .../webview_android_widget_test.mocks.dart | 443 +++++++++++++ .../analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + 37 files changed, 2219 insertions(+), 1741 deletions(-) delete mode 100644 packages/webview_flutter/analysis_options.yaml create mode 100644 packages/webview_flutter/webview_flutter/analysis_options.yaml delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/analysis_options.yaml create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/analysis_options.yaml diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/webview_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter/analysis_options.yaml b/packages/webview_flutter/webview_flutter/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml 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 0baffba0edb7..0e7e3451d054 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 @@ -197,9 +197,11 @@ void main() { onPageFinished: () => onPageFinished.complete(), )); await onPageFinished.future; - if (Platform.isAndroid) { - await initialResizeCompleter.future; - } + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); resizeButtonTapped = true; await tester.tap(find.byKey(const ValueKey('resizeButton'))); diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index b241be63248c..09a797f4a1ed 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Replaces platform implementation with API built with pigeon. + ## 2.2.1 * Fix `NullPointerException` from a race condition when changing focus. This only affects `WebView` diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java deleted file mode 100644 index cfad4e315514..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.webkit.DownloadListener; -import android.webkit.WebView; - -/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ -public class FlutterDownloadListener implements DownloadListener { - private final FlutterWebViewClient webViewClient; - private WebView webView; - - public FlutterDownloadListener(FlutterWebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ - public void setWebView(WebView webView) { - this.webView = webView; - } - - @Override - public void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - long contentLength) { - webViewClient.notifyDownload(webView, url); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index ed14107220b8..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,495 +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.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - flutterWebViewClient.onLoadingProgress(progress); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - MethodChannel methodChannel, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - - FlutterDownloadListener flutterDownloadListener = - new FlutterDownloadListener(flutterWebViewClient); - webView = - createWebView( - new WebViewBuilder(context, containerView), - params, - new FlutterWebChromeClient(), - flutterDownloadListener); - flutterDownloadListener.setWebView(webView); - - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - - Map settings = (Map) params.get("settings"); - if (settings != null) { - applySettings(settings); - } - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) { - registerJavaScriptChannelNames(names); - } - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) { - updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - } - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - /** - * Creates a {@link android.webkit.WebView} and configures it according to the supplied - * parameters. - * - *

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

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

Important: This method is visible for testing purposes only and should - * never be called from outside this class. - * - * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link - * WebView}. - * @param params creation parameters received over the method channel. - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return The new {@link android.webkit.WebView} object. - */ - @VisibleForTesting - static WebView createWebView( - WebViewBuilder webViewBuilder, - Map params, - WebChromeClient webChromeClient, - @Nullable DownloadListener downloadListener) { - boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); - webViewBuilder - .setUsesHybridComposition(usesHybridComposition) - .setDomStorageEnabled(true) // Always enable DOM storage API. - .setJavaScriptCanOpenWindowsAutomatically( - true) // Always allow automatically opening of windows. - .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient( - webChromeClient) // Always use {@link FlutterWebChromeClient} as web Chrome client. - .setDownloadListener(downloadListener) - .setZoomControlsEnabled(true); // Always use built-in zoom mechanisms. - - return webViewBuilder.build(); - } - - @Override - public View getView() { - return webView; - } - - @Override - public void onInputConnectionUnlocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).unlockInputConnection(); - } - } - - @Override - public void onInputConnectionLocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).lockInputConnection(); - } - } - - @Override - public void onFlutterViewAttached(View flutterView) { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(flutterView); - } - } - - @Override - public void onFlutterViewDetached() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(null); - } - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - case "runJavascriptReturningResult": - evaluateJavaScript(methodCall, result, true); - break; - case "runJavascript": - evaluateJavaScript(methodCall, result, false); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript( - MethodCall methodCall, final Result result, final boolean returnValue) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - if (returnValue) { - result.success(value); - } else { - result.success(null); - } - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) { - updateJsMode(mode); - } - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "hasProgressTracking": - flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "allowsInlineMediaPlayback": - // no-op inline media playback is always allowed on Android. - break; - case "zoomEnabled": - setZoomEnabled((boolean) settings.get(key)); - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - private void setZoomEnabled(boolean shouldEnable) { - webView.getSettings().setSupportZoom(shouldEnable); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).dispose(); - } - webView.destroy(); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index a86d3e8a4b63..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,323 +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.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - boolean hasProgressTracking; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - /** - * Notifies the Flutter code that a download should start when a navigation delegate is set. - * - * @param view the webView the result of the navigation delegate will be send to. - * @param url the download url - * @return A boolean whether or not the request is forwarded to the Flutter code. - */ - boolean notifyDownload(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - void onLoadingProgress(int progress) { - if (hasProgressTracking) { - Map args = new HashMap<>(); - args.put("progress", progress); - methodChannel.invokeMethod("onProgress", args); - } - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java index 8fe58104a0fb..9b3cd471bb83 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -5,29 +5,24 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; -public final class FlutterWebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; +class FlutterWebViewFactory extends PlatformViewFactory { + private final InstanceManager instanceManager; - FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + FlutterWebViewFactory(InstanceManager instanceManager) { super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; + this.instanceManager = instanceManager; } - @SuppressWarnings("unchecked") @Override public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - return new FlutterWebView(context, methodChannel, params, containerView); + final PlatformView view = (PlatformView) instanceManager.getInstance((Integer) args); + if (view == null) { + throw new IllegalStateException("Unable to find WebView instance: " + args); + } + return view; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index 96ae3ce1fd4d..ce6f2b81ed1c 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -9,8 +9,6 @@ import android.webkit.JavascriptInterface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; /** * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets @@ -22,24 +20,10 @@ *

No messages are sent to Dart after {@link JavaScriptChannel#release} is called. */ public class JavaScriptChannel implements Releasable { - private final MethodChannel methodChannel; private final Handler platformThreadHandler; final String javaScriptChannelName; @Nullable private JavaScriptChannelFlutterApiImpl flutterApi; - /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through - */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; - this.platformThreadHandler = platformThreadHandler; - } - /** * Creates a {@link JavaScriptChannel} that passes arguments of callback methods to Dart. * @@ -54,36 +38,23 @@ public JavaScriptChannel( this.flutterApi = flutterApi; this.javaScriptChannelName = channelName; this.platformThreadHandler = platformThreadHandler; - methodChannel = null; } // Suppressing unused warning as this is invoked from JavaScript. @SuppressWarnings("unused") @JavascriptInterface public void postMessage(final String message) { - Runnable postMessageRunnable = null; - - if (flutterApi != null) { - postMessageRunnable = () -> flutterApi.postMessage(this, message, reply -> {}); - } else if (methodChannel != null) { - postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - } + final Runnable postMessageRunnable = + () -> { + if (flutterApi != null) { + flutterApi.postMessage(JavaScriptChannel.this, message, reply -> {}); + } + }; - if (postMessageRunnable != null) { - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java index ac67a817179e..3055c9fc7f40 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java @@ -16,7 +16,8 @@ public class JavaScriptChannelHostApiImpl implements JavaScriptChannelHostApi { private final InstanceManager instanceManager; private final JavaScriptChannelCreator javaScriptChannelCreator; private final JavaScriptChannelFlutterApiImpl flutterApi; - private final Handler platformThreadHandler; + + private Handler platformThreadHandler; /** Handles creating {@link JavaScriptChannel}s for a {@link JavaScriptChannelHostApiImpl}. */ public static class JavaScriptChannelCreator { @@ -55,6 +56,15 @@ public JavaScriptChannelHostApiImpl( this.platformThreadHandler = platformThreadHandler; } + /** + * Sets the platformThreadHandler to make callbacks + * + * @param platformThreadHandler the new thread handler + */ + public void setPlatformThreadHandler(Handler platformThreadHandler) { + this.platformThreadHandler = platformThreadHandler; + } + @Override public void create(Long instanceId, String channelName) { final JavaScriptChannel javaScriptChannel = diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index f8bc5129a3ca..d2e1e59ce179 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; /** @@ -44,32 +45,56 @@ public WebChromeClientImpl( this.webViewClient = webViewClient; } - // Verifies that a url opened by `Window.open` has a secure url. @Override public boolean onCreateWindow( final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient newWindowWebViewClient = + return onCreateWindow(view, resultMsg, new WebView(view.getContext())); + } + + /** + * Verifies that a url opened by `Window.open` has a secure url. + * + * @param view the WebView from which the request for a new window originated. + * @param resultMsg the message to send when once a new WebView has been created. resultMsg.obj + * is a {@link WebView.WebViewTransport} object. This should be used to transport the new + * WebView, by calling WebView.WebViewTransport.setWebView(WebView) + * @param onCreateWindowWebView the temporary WebView used to verify the url is secure + * @return this method should return true if the host application will create a new window, in + * which case resultMsg should be sent to its target. Otherwise, this method should return + * false. Returning false from this method but also sending resultMsg will result in + * undefined behavior + */ + @VisibleForTesting + boolean onCreateWindow( + final WebView view, Message resultMsg, @Nullable WebView onCreateWindowWebView) { + final WebViewClient windowWebViewClient = new WebViewClient() { @RequiresApi(api = Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - webViewClient.shouldOverrideUrlLoading(view, request); + @NonNull WebView windowWebView, @NonNull WebResourceRequest request) { + if (!webViewClient.shouldOverrideUrlLoading(view, request)) { + view.loadUrl(request.getUrl().toString()); + } return true; } @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClient.shouldOverrideUrlLoading(view, url); + public boolean shouldOverrideUrlLoading(WebView windowWebView, String url) { + if (!webViewClient.shouldOverrideUrlLoading(view, url)) { + view.loadUrl(url); + } return true; } }; - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(newWindowWebViewClient); + if (onCreateWindowWebView == null) { + onCreateWindowWebView = new WebView(view.getContext()); + } + onCreateWindowWebView.setWebViewClient(windowWebViewClient); final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); + transport.setWebView(onCreateWindowWebView); resultMsg.sendToTarget(); return true; diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java deleted file mode 100644 index e0d5e8815f31..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ /dev/null @@ -1,172 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** Builder used to create {@link android.webkit.WebView} objects. */ -public class WebViewBuilder { - - /** Factory used to create a new {@link android.webkit.WebView} instance. */ - static class WebViewFactory { - - /** - * Creates a new {@link android.webkit.WebView} instance. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is - * returned. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set - * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or - * IME, thread (see also {@link InputAwareWebView}) - * @return A new instance of the {@link android.webkit.WebView} object. - */ - static WebView create(Context context, boolean usesHybridComposition, View containerView) { - return usesHybridComposition - ? new WebView(context) - : new InputAwareWebView(context, containerView); - } - } - - private final Context context; - private final View containerView; - - private boolean enableDomStorage; - private boolean javaScriptCanOpenWindowsAutomatically; - private boolean supportMultipleWindows; - private boolean usesHybridComposition; - private WebChromeClient webChromeClient; - private DownloadListener downloadListener; - private boolean enableBuiltInZoomControls; - - /** - * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link - * WebViewFactory} object. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to - * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, - * thread (see also {@link InputAwareWebView}) - */ - WebViewBuilder(@NonNull final Context context, View containerView) { - this.context = context; - this.containerView = containerView; - } - - /** - * Sets whether the DOM storage API is enabled. The default value is {@code false}. - * - * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDomStorageEnabled(boolean flag) { - this.enableDomStorage = flag; - return this; - } - - /** - * Sets whether JavaScript is allowed to open windows automatically. This applies to the - * JavaScript function {@code window.open()}. The default value is {@code false}. - * - * @param flag {@code true} if JavaScript is allowed to open windows automatically. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { - this.javaScriptCanOpenWindowsAutomatically = flag; - return this; - } - - /** - * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link - * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is - * {@code false}. - * - * @param flag {@code true} if multiple windows are supported. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setSupportMultipleWindows(boolean flag) { - this.supportMultipleWindows = flag; - return this; - } - - /** - * Sets whether the hybrid composition should be used. - * - *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the - * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the - * {@link WebView} on Android versions below N. - * - * @param flag {@code true} if uses hybrid composition. The default is {@code false}. - * @return This builder. This value cannot be {@code null} - */ - public WebViewBuilder setUsesHybridComposition(boolean flag) { - this.usesHybridComposition = flag; - return this; - } - - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling - * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. - * - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { - this.webChromeClient = webChromeClient; - return this; - } - - /** - * Registers the interface to be used when content can not be handled by the rendering engine, and - * should be downloaded instead. This will replace the current handler. - * - * @param downloadListener an implementation of DownloadListener This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { - this.downloadListener = downloadListener; - return this; - } - - /** - * Sets whether the {@link WebView} should use its built-in zoom mechanisms. The default value is - * {@code true}. - * - * @param flag {@code true} if built in zoom controls are allowed. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setZoomControlsEnabled(boolean flag) { - this.enableBuiltInZoomControls = flag; - return this; - } - - /** - * Build the {@link android.webkit.WebView} using the current settings. - * - * @return The {@link android.webkit.WebView} using the current settings. - */ - public WebView build() { - WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); - - WebSettings webSettings = webView.getSettings(); - webSettings.setDomStorageEnabled(enableDomStorage); - webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); - webSettings.setSupportMultipleWindows(supportMultipleWindows); - webSettings.setLoadWithOverviewMode(true); - webSettings.setUseWideViewPort(true); - webSettings.setDisplayZoomControls(false); - webSettings.setBuiltInZoomControls(enableBuiltInZoomControls); - webView.setWebChromeClient(webChromeClient); - webView.setDownloadListener(downloadListener); - return webView; - } -} 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 268d35a1e04c..2b174ff35e6f 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 @@ -4,20 +4,34 @@ package io.flutter.plugins.webviewflutter; +import android.content.Context; +import android.os.Handler; +import android.view.View; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +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.WebViewClientHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; /** * Java platform implementation of the webview_flutter plugin. * *

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

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} - * package instead. + *

Call {@link #registerWith} to use the stable {@code io.flutter.plugin.common} package instead. */ -public class WebViewFlutterPlugin implements FlutterPlugin { - +public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { + private FlutterPluginBinding pluginBinding; private FlutterCookieManager flutterCookieManager; + private WebViewHostApiImpl webViewHostApi; + private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; /** * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to @@ -26,7 +40,7 @@ public class WebViewFlutterPlugin implements FlutterPlugin { *

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

Registration should eventually be handled automatically by v2 of the * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694 @@ -38,31 +52,79 @@ public WebViewFlutterPlugin() {} * package. * *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. + * won't react to changes in activity or context, unlike {@link WebViewFlutterPlugin}. */ - @SuppressWarnings("deprecation") + @SuppressWarnings({"unused", "deprecation"}) public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(registrar.messenger(), registrar.view())); + new WebViewFlutterPlugin() + .setUp( + registrar.messenger(), + registrar.platformViewRegistry(), + registrar.activity(), + registrar.view()); new FlutterCookieManager(registrar.messenger()); } + private void setUp( + BinaryMessenger binaryMessenger, + PlatformViewRegistry viewRegistry, + Context context, + View containerView) { + new FlutterCookieManager(binaryMessenger); + + InstanceManager instanceManager = new InstanceManager(); + + viewRegistry.registerViewFactory( + "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); + + webViewHostApi = + new WebViewHostApiImpl( + instanceManager, new WebViewHostApiImpl.WebViewProxy(), context, containerView); + javaScriptChannelHostApi = + new JavaScriptChannelHostApiImpl( + instanceManager, + new JavaScriptChannelHostApiImpl.JavaScriptChannelCreator(), + new JavaScriptChannelFlutterApiImpl(binaryMessenger, instanceManager), + new Handler(context.getMainLooper())); + + WebViewHostApi.setup(binaryMessenger, webViewHostApi); + JavaScriptChannelHostApi.setup(binaryMessenger, javaScriptChannelHostApi); + WebViewClientHostApi.setup( + binaryMessenger, + new WebViewClientHostApiImpl( + instanceManager, + new WebViewClientHostApiImpl.WebViewClientCreator(), + new WebViewClientFlutterApiImpl(binaryMessenger, instanceManager))); + WebChromeClientHostApi.setup( + binaryMessenger, + new WebChromeClientHostApiImpl( + instanceManager, + new WebChromeClientHostApiImpl.WebChromeClientCreator(), + new WebChromeClientFlutterApiImpl(binaryMessenger, instanceManager))); + DownloadListenerHostApi.setup( + binaryMessenger, + new DownloadListenerHostApiImpl( + instanceManager, + new DownloadListenerHostApiImpl.DownloadListenerCreator(), + new DownloadListenerFlutterApiImpl(binaryMessenger, instanceManager))); + WebSettingsHostApi.setup( + binaryMessenger, + new WebSettingsHostApiImpl( + instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + } + @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + pluginBinding = binding; + setUp( + binding.getBinaryMessenger(), + binding.getPlatformViewRegistry(), + binding.getApplicationContext(), + null); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { if (flutterCookieManager == null) { return; } @@ -70,4 +132,30 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { flutterCookieManager.dispose(); flutterCookieManager = null; } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + updateContext(pluginBinding.getApplicationContext()); + } + + @Override + public void onReattachedToActivityForConfigChanges( + @NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivity() { + updateContext(pluginBinding.getApplicationContext()); + } + + private void updateContext(Context context) { + webViewHostApi.setContext(context); + javaScriptChannelHostApi.setPlatformThreadHandler(new Handler(context.getMainLooper())); + } } 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 2d96fc3f8a60..86bef0fab563 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 @@ -35,10 +35,11 @@ public class WebViewHostApiImpl implements WebViewHostApi { private final InstanceManager instanceManager; private final WebViewProxy webViewProxy; - private final Context context; // Only used with WebView using virtual displays. @Nullable private final View containerView; + private Context context; + /** Handles creating and calling static methods for {@link WebView}s. */ public static class WebViewProxy { /** @@ -317,6 +318,15 @@ public WebViewHostApiImpl( this.containerView = containerView; } + /** + * Sets the context to construct {@link WebView}s. + * + * @param context the new context. + */ + public void setContext(Context context) { + this.context = context; + } + @Override public void create(Long instanceId, Boolean useHybridComposition) { DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); @@ -335,9 +345,10 @@ public void create(Long instanceId, Boolean useHybridComposition) { @Override public void dispose(Long instanceId) { - final WebView instance = (WebView) instanceManager.removeInstanceWithId(instanceId); + final WebView instance = (WebView) instanceManager.getInstance(instanceId); if (instance != null) { ((Releasable) instance).release(); + instanceManager.removeInstance(instance); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java deleted file mode 100644 index 2c918584ba83..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java +++ /dev/null @@ -1,42 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.webkit.WebView; -import org.junit.Before; -import org.junit.Test; - -public class FlutterDownloadListenerTest { - private FlutterWebViewClient webViewClient; - private WebView webView; - - @Before - public void before() { - webViewClient = mock(FlutterWebViewClient.class); - webView = mock(WebView.class); - } - - @Test - public void onDownloadStart_should_notify_webViewClient() { - String url = "testurl.com"; - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); - } - - @Test - public void onDownloadStart_should_pass_webView() { - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.setWebView(webView); - downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(eq(webView), anyString()); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java deleted file mode 100644 index 86346ac08f16..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ /dev/null @@ -1,60 +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.webviewflutter; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import android.webkit.WebView; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class FlutterWebViewClientTest { - - MethodChannel mockMethodChannel; - WebView mockWebView; - - @Before - public void before() { - mockMethodChannel = mock(MethodChannel.class); - mockWebView = mock(WebView.class); - } - - @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(true); - - client.notifyDownload(mockWebView, url); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(mockMethodChannel) - .invokeMethod( - eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); - HashMap map = (HashMap) argumentCaptor.getValue(); - assertEquals(map.get("url"), url); - assertEquals(map.get("isForMainFrame"), true); - } - - @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(false); - - client.notifyDownload(mockWebView, url); - verifyNoInteractions(mockMethodChannel); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java deleted file mode 100644 index f26a0ea6b9cc..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ /dev/null @@ -1,173 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; - -public class FlutterWebViewTest { - private WebChromeClient mockWebChromeClient; - private DownloadListener mockDownloadListener; - private WebViewBuilder mockWebViewBuilder; - private WebView mockWebView; - private MethodChannel.Result mockResult; - private Context mockContext; - private MethodChannel mockMethodChannel; - - @Before - public void before() { - - mockWebChromeClient = mock(WebChromeClient.class); - mockWebViewBuilder = mock(WebViewBuilder.class); - mockWebView = mock(WebView.class); - mockDownloadListener = mock(DownloadListener.class); - mockResult = mock(MethodChannel.Result.class); - mockContext = mock(Context.class); - mockMethodChannel = mock(MethodChannel.class); - - when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setZoomControlsEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.build()).thenReturn(mockWebView); - } - - @Test - public void createWebView_shouldCreateWebViewWithDefaultConfiguration() { - FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); - - verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); - verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); - verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); - verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); - verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true); - } - - @Test(expected = UnsupportedOperationException.class) - public void evaluateJavaScript_shouldThrowForNullString() { - try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { - // Setup - mockedFlutterWebView - .when( - new MockedStatic.Verification() { - @Override - public void apply() throws Throwable { - FlutterWebView.createWebView( - (WebViewBuilder) any(), - (Map) any(), - (WebChromeClient) any(), - (DownloadListener) any()); - } - }) - .thenReturn(mockWebView); - FlutterWebView flutterWebView = - new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); - - // Run - flutterWebView.onMethodCall(new MethodCall("runJavascript", null), mockResult); - } - } - - @Test - public void evaluateJavaScript_shouldReturnValueOnSuccessForReturnValue() { - try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { - // Setup - mockedFlutterWebView - .when( - () -> - FlutterWebView.createWebView( - (WebViewBuilder) any(), - (Map) any(), - (WebChromeClient) any(), - (DownloadListener) any())) - .thenReturn(mockWebView); - doAnswer( - invocation -> { - android.webkit.ValueCallback callback = invocation.getArgument(1); - callback.onReceiveValue("Test JavaScript Result"); - return null; - }) - .when(mockWebView) - .evaluateJavascript(eq("Test JavaScript String"), any()); - FlutterWebView flutterWebView = - new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); - - // Run - flutterWebView.onMethodCall( - new MethodCall("runJavascriptReturningResult", "Test JavaScript String"), mockResult); - - // Verify - verify(mockResult, times(1)).success("Test JavaScript Result"); - } - } - - @Test - public void evaluateJavaScript_shouldReturnNilOnSuccessForNoReturnValue() { - try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { - // Setup - mockedFlutterWebView - .when( - () -> - FlutterWebView.createWebView( - (WebViewBuilder) any(), - (Map) any(), - (WebChromeClient) any(), - (DownloadListener) any())) - .thenReturn(mockWebView); - doAnswer( - invocation -> { - android.webkit.ValueCallback callback = invocation.getArgument(1); - callback.onReceiveValue("Test JavaScript Result"); - return null; - }) - .when(mockWebView) - .evaluateJavascript(eq("Test JavaScript String"), any()); - FlutterWebView flutterWebView = - new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); - - // Run - flutterWebView.onMethodCall( - new MethodCall("runJavascript", "Test JavaScript String"), mockResult); - - // Verify - verify(mockResult, times(1)).success(isNull()); - } - } - - private Map createParameterMap(boolean usesHybridComposition) { - Map params = new HashMap<>(); - params.put("usesHybridComposition", usesHybridComposition); - - return params; - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index d6593ab24462..63cd31043799 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -4,20 +4,29 @@ package io.flutter.plugins.webviewflutter; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.net.Uri; +import android.os.Message; import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; import android.webkit.WebView; +import android.webkit.WebView.WebViewTransport; import android.webkit.WebViewClient; import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator; import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -66,4 +75,43 @@ public void onProgressChanged() { webChromeClient.onProgressChanged(mockWebView, 11); verify(mockFlutterApi, never()).onProgressChanged((WebChromeClient) any(), any(), any(), any()); } + + @Test + public void onCreateWindow() { + final WebView mockOnCreateWindowWebView = mock(WebView.class); + + // Create a fake message to transport requests to onCreateWindowWebView. + final Message message = new Message(); + message.obj = mock(WebViewTransport.class); + + assertTrue(webChromeClient.onCreateWindow(mockWebView, message, mockOnCreateWindowWebView)); + + /// Capture the WebViewClient used with onCreateWindow WebView. + final ArgumentCaptor webViewClientCaptor = + ArgumentCaptor.forClass(WebViewClient.class); + verify(mockOnCreateWindowWebView).setWebViewClient(webViewClientCaptor.capture()); + final WebViewClient onCreateWindowWebViewClient = webViewClientCaptor.getValue(); + assertNotNull(onCreateWindowWebViewClient); + + /// Create a WebResourceRequest with a Uri. + final WebResourceRequest mockRequest = mock(WebResourceRequest.class); + when(mockRequest.getUrl()).thenReturn(mock(Uri.class)); + when(mockRequest.getUrl().toString()).thenReturn("https://www.google.com"); + + // Test when the forwarding WebViewClient is overriding all url loading. + when(mockWebViewClient.shouldOverrideUrlLoading(any(), any(WebResourceRequest.class))) + .thenReturn(true); + assertTrue( + onCreateWindowWebViewClient.shouldOverrideUrlLoading( + mockOnCreateWindowWebView, mockRequest)); + verify(mockWebView, never()).loadUrl(any()); + + // Test when the forwarding WebViewClient is NOT overriding all url loading. + when(mockWebViewClient.shouldOverrideUrlLoading(any(), any(WebResourceRequest.class))) + .thenReturn(false); + assertTrue( + onCreateWindowWebViewClient.shouldOverrideUrlLoading( + mockOnCreateWindowWebView, mockRequest)); + verify(mockWebView).loadUrl("https://www.google.com"); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java deleted file mode 100644 index 423cb210c392..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ /dev/null @@ -1,104 +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.webviewflutter; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; -import java.io.IOException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; -import org.mockito.MockedStatic.Verification; - -public class WebViewBuilderTest { - private Context mockContext; - private View mockContainerView; - private WebView mockWebView; - private MockedStatic mockedStaticWebViewFactory; - - @Before - public void before() { - mockContext = mock(Context.class); - mockContainerView = mock(View.class); - mockWebView = mock(WebView.class); - mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); - - mockedStaticWebViewFactory - .when( - new Verification() { - @Override - public void apply() { - WebViewFactory.create(mockContext, false, mockContainerView); - } - }) - .thenReturn(mockWebView); - } - - @After - public void after() { - mockedStaticWebViewFactory.close(); - } - - @Test - public void ctor_test() { - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - assertNotNull(builder); - } - - @Test - public void build_should_set_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - DownloadListener mockDownloadListener = mock(DownloadListener.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = - new WebViewBuilder(mockContext, mockContainerView) - .setDomStorageEnabled(true) - .setJavaScriptCanOpenWindowsAutomatically(true) - .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient) - .setDownloadListener(mockDownloadListener); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(true); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebSettings).setSupportMultipleWindows(true); - verify(mockWebView).setWebChromeClient(mockWebChromeClient); - verify(mockWebView).setDownloadListener(mockDownloadListener); - } - - @Test - public void build_should_use_default_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(false); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); - verify(mockWebSettings).setSupportMultipleWindows(false); - verify(mockWebView).setWebChromeClient(null); - verify(mockWebView).setDownloadListener(null); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index 454e2c959680..91fc860dffae 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 @@ -192,7 +192,6 @@ void main() { bool resizeButtonTapped = false; await tester.pumpWidget(ResizableWebView( onResize: (_) { - print(resizeButtonTapped); if (resizeButtonTapped) { buttonTapResizeCompleter.complete(); } else { @@ -202,10 +201,14 @@ void main() { onPageFinished: () => onPageFinished.complete(), )); await onPageFinished.future; - await initialResizeCompleter.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); resizeButtonTapped = true; - await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.tap(find.byKey(const ValueKey('resizeButton'))); await tester.pumpAndSettle(); expect(buttonTapResizeCompleter.future, completes); }); @@ -461,10 +464,10 @@ void main() { testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -495,7 +498,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -504,7 +507,7 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = + final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -682,7 +685,7 @@ void main() { }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' Some title @@ -726,7 +729,7 @@ void main() { group('Programmatic Scroll', () { // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -773,7 +776,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -813,7 +816,7 @@ void main() { // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -860,7 +863,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); // Check scrollTo() const int X_SCROLL = 123; @@ -883,7 +886,7 @@ void main() { // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -935,7 +938,7 @@ void main() { ), ), ); - await Future.delayed(Duration(milliseconds: 20)); + await Future.delayed(const Duration(milliseconds: 20)); await tester.pump(); }); @@ -944,7 +947,7 @@ void main() { final String viewportRectJSON = await _runJavaScriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON); + jsonDecode(viewportRectJSON) as Map; // Check that the input is originally outside of the viewport. @@ -952,7 +955,7 @@ void main() { await _runJavaScriptReturningResult( controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON); + jsonDecode(initialInputClientRectJSON) as Map; expect( initialInputClientRectRelativeToViewport['bottom'] <= @@ -967,7 +970,7 @@ void main() { await _runJavaScriptReturningResult( controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON); + jsonDecode(lastInputClientRectJSON) as Map; expect( lastInputClientRectRelativeToViewport['top'] >= @@ -990,7 +993,7 @@ void main() { }); group('NavigationDelegate', () { - final String blankPage = ""; + const String blankPage = ''; final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + base64Encode(const Utf8Encoder().convert(blankPage)); @@ -1081,7 +1084,7 @@ void main() { testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' @@ -1294,7 +1297,7 @@ void main() { testWidgets( 'JavaScript does not run in parent window', (WidgetTester tester) async { - final String iframe = ''' + const String iframe = ''' - - - diff --git a/packages/connectivity/connectivity/example/web/manifest.json b/packages/connectivity/connectivity/example/web/manifest.json deleted file mode 100644 index 8c012917dab7..000000000000 --- a/packages/connectivity/connectivity/example/web/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "example", - "short_name": "example", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} diff --git a/packages/connectivity/connectivity/ios/Assets/.gitkeep b/packages/connectivity/connectivity/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h deleted file mode 100644 index aec76adc0b58..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTConnectivityPlugin : NSObject -@end diff --git a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m deleted file mode 100644 index ac37c2359137..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTConnectivityPlugin.h" - -#import "Reachability/Reachability.h" - -#import -#import "SystemConfiguration/CaptiveNetwork.h" - -#include - -#include - -@interface FLTConnectivityPlugin () - -@end - -@implementation FLTConnectivityPlugin { - FlutterEventSink _eventSink; - Reachability* _reachabilityForInternetConnection; -} - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTConnectivityPlugin* instance = [[FLTConnectivityPlugin alloc] init]; - - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/connectivity" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; - - FlutterEventChannel* streamChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/connectivity_status" - binaryMessenger:[registrar messenger]]; - [streamChannel setStreamHandler:instance]; -} - -- (NSString*)statusFromReachability:(Reachability*)reachability { - NetworkStatus status = [reachability currentReachabilityStatus]; - switch (status) { - case NotReachable: - return @"none"; - case ReachableViaWiFi: - return @"wifi"; - case ReachableViaWWAN: - return @"mobile"; - } -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"check"]) { - // This is supposed to be quick. Another way of doing this would be to - // signup for network - // connectivity changes. However that depends on the app being in background - // and the code - // gets more involved. So for now, this will do. - result([self statusFromReachability:[Reachability reachabilityForInternetConnection]]); - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onReachabilityDidChange:(NSNotification*)notification { - Reachability* curReach = [notification object]; - _eventSink([self statusFromReachability:curReach]); -} - -#pragma mark FlutterStreamHandler impl - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _eventSink = eventSink; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onReachabilityDidChange:) - name:kReachabilityChangedNotification - object:nil]; - _reachabilityForInternetConnection = [Reachability reachabilityForInternetConnection]; - [_reachabilityForInternetConnection startNotifier]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - if (_reachabilityForInternetConnection) { - [_reachabilityForInternetConnection stopNotifier]; - _reachabilityForInternetConnection = nil; - } - [[NSNotificationCenter defaultCenter] removeObserver:self]; - _eventSink = nil; - return nil; -} - -@end diff --git a/packages/connectivity/connectivity/ios/connectivity.podspec b/packages/connectivity/connectivity/ios/connectivity.podspec deleted file mode 100644 index 096a76b8d3c3..000000000000 --- a/packages/connectivity/connectivity/ios/connectivity.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity' - s.version = '0.0.1' - s.summary = 'Flutter Connectivity' - s.description = <<-DESC -This plugin allows Flutter apps to discover network connectivity and configure themselves accordingly. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity' } - s.documentation_url = 'https://pub.dev/packages/connectivity' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.dependency 'Reachability' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/connectivity/connectivity/lib/connectivity.dart b/packages/connectivity/connectivity/lib/connectivity.dart deleted file mode 100644 index 1b819d7470d2..000000000000 --- a/packages/connectivity/connectivity/lib/connectivity.dart +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -// Export enums from the platform_interface so plugin users can use them directly. -export 'package:connectivity_platform_interface/connectivity_platform_interface.dart' - show ConnectivityResult, LocationAuthorizationStatus; - -/// Discover network connectivity configurations: Distinguish between WI-FI and cellular, check WI-FI status and more. -class Connectivity { - /// Constructs a singleton instance of [Connectivity]. - /// - /// [Connectivity] is designed to work as a singleton. - // When a second instance is created, the first instance will not be able to listen to the - // EventChannel because it is overridden. Forcing the class to be a singleton class can prevent - // misuse of creating a second instance from a programmer. - factory Connectivity() { - if (_singleton == null) { - _singleton = Connectivity._(); - } - return _singleton!; - } - - Connectivity._(); - - static Connectivity? _singleton; - - static ConnectivityPlatform get _platform => ConnectivityPlatform.instance; - - /// Fires whenever the connectivity state changes. - Stream get onConnectivityChanged { - return _platform.onConnectivityChanged; - } - - /// Checks the connection status of the device. - /// - /// Do not use the result of this function to decide whether you can reliably - /// make a network request. It only gives you the radio status. - /// - /// Instead listen for connectivity changes via [onConnectivityChanged] stream. - Future checkConnectivity() { - return _platform.checkConnectivity(); - } -} diff --git a/packages/connectivity/connectivity/pubspec.yaml b/packages/connectivity/connectivity/pubspec.yaml deleted file mode 100644 index 16e179cfa085..000000000000 --- a/packages/connectivity/connectivity/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: connectivity -description: Flutter plugin for discovering the state of the network (WiFi & - mobile/cellular) connectivity on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 3.0.6 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.connectivity - pluginClass: ConnectivityPlugin - ios: - pluginClass: FLTConnectivityPlugin - macos: - default_package: connectivity_macos - web: - default_package: connectivity_for_web - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - connectivity_platform_interface: ^2.0.0 - connectivity_macos: ^0.2.0 - connectivity_for_web: ^0.4.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - plugin_platform_interface: ^2.0.0 - pedantic: ^1.10.0 - test: ^1.16.3 diff --git a/packages/connectivity/connectivity/test/connectivity_test.dart b/packages/connectivity/connectivity/test/connectivity_test.dart deleted file mode 100644 index e6c253e0d49a..000000000000 --- a/packages/connectivity/connectivity/test/connectivity_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:connectivity/connectivity.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; - -const ConnectivityResult kCheckConnectivityResult = ConnectivityResult.wifi; -const LocationAuthorizationStatus kRequestLocationResult = - LocationAuthorizationStatus.authorizedAlways; -const LocationAuthorizationStatus kGetLocationResult = - LocationAuthorizationStatus.authorizedAlways; - -void main() { - group('Connectivity', () { - late Connectivity connectivity; - late MockConnectivityPlatform fakePlatform; - setUp(() async { - fakePlatform = MockConnectivityPlatform(); - ConnectivityPlatform.instance = fakePlatform; - connectivity = Connectivity(); - }); - - test('checkConnectivity', () async { - ConnectivityResult result = await connectivity.checkConnectivity(); - expect(result, kCheckConnectivityResult); - }); - }); -} - -class MockConnectivityPlatform extends Fake - with MockPlatformInterfaceMixin - implements ConnectivityPlatform { - Future checkConnectivity() async { - return kCheckConnectivityResult; - } -} diff --git a/packages/connectivity/connectivity_for_web/.gitignore b/packages/connectivity/connectivity_for_web/.gitignore deleted file mode 100644 index d7dee828a6b9..000000000000 --- a/packages/connectivity/connectivity_for_web/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -.dart_tool/ - -.packages -.pub/ - -build/ -lib/generated_plugin_registrant.dart diff --git a/packages/connectivity/connectivity_for_web/.metadata b/packages/connectivity/connectivity_for_web/.metadata deleted file mode 100644 index 23eb55ba6da2..000000000000 --- a/packages/connectivity/connectivity_for_web/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 52ee8a6c6565cd421dfa32042941eb40691f4746 - channel: master - -project_type: plugin diff --git a/packages/connectivity/connectivity_for_web/AUTHORS b/packages/connectivity/connectivity_for_web/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/connectivity/connectivity_for_web/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_for_web/CHANGELOG.md b/packages/connectivity/connectivity_for_web/CHANGELOG.md deleted file mode 100644 index 97e5032c8dd4..000000000000 --- a/packages/connectivity/connectivity_for_web/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -## 0.4.0+1 - -* Add `implements` to pubspec. - -## 0.4.0 - -* Migrate to null-safety -* Run tests using flutter driver - -## 0.3.1+4 - -* Remove unused `test` dependency. - -## 0.3.1+3 - -* Fix homepage in `pubspec.yaml`. - -## 0.3.1+2 - -* Update package:e2e to use package:integration_test - -## 0.3.1+1 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.3.1 - -* Use NetworkInformation API from dart:html, instead of the JS-interop version. - -## 0.3.0 - -* Rename from "experimental_connectivity_web" to "connectivity_for_web", and move to flutter/plugins master. - -## 0.2.0 - -* Add fallback on dart:html for browsers where NetworkInformationAPI is not supported. - -## 0.1.0 - -* Initial release. diff --git a/packages/connectivity/connectivity_for_web/LICENSE b/packages/connectivity/connectivity_for_web/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/connectivity/connectivity_for_web/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/connectivity/connectivity_for_web/README.md b/packages/connectivity/connectivity_for_web/README.md deleted file mode 100644 index 66efc49fc840..000000000000 --- a/packages/connectivity/connectivity_for_web/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# connectivity_for_web - -A web implementation of [connectivity](https://pub.dev/connectivity/connectivity). Currently this package uses an experimental API, with a fallback to dart:html, so not all features may be available to all browsers. - -## Usage - -### Import the package - -This package is a non-endorsed implementation of `connectivity` for the web platform, so you need to modify your `pubspec.yaml` to use it: - -```yaml -... -dependencies: - ... - connectivity: ^0.4.9 - connectivity_for_web: ^0.3.0 - ... -... -``` - -## Example - -Find the example wiring in the [Google sign-in example application](https://github.com/ditman/plugins/blob/connectivity-web/packages/connectivity/connectivity/example/lib/main.dart). - -## Limitations on the web platform - -In order to retrieve information about the quality/speed of a browser's connection, the web implementation of the `connectivity` plugin uses the browser's [**NetworkInformation** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation), which as of this writing (June 2020) is still "experimental", and not available in all browsers: - -![Data on support for the netinfo feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/netinfo.png) - -On desktop browsers, this API only returns a very broad set of connectivity statuses (One of `'slow-2g', '2g', '3g', or '4g'`), and may *not* provide a Stream of changes. Firefox still hasn't enabled this feature by default. - -**Fallback to `navigator.onLine`** - -For those browsers where the NetworkInformation Web API is not available, the plugin falls back to the [**NavigatorOnLine** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine), which is more broadly supported: - -![Data on support for the online-status feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/online-status.png) - - -The NavigatorOnLine API is [provided by `dart:html`](https://api.dart.dev/stable/2.7.2/dart-html/Navigator/onLine.html), and only supports a boolean connectivity status (either online or offline), with no network speed information. In those cases the plugin will return either `wifi` (when the browser is online) or `none` (when it's not). - -Other than the approximate "downlink" speed, where available, and due to security and privacy concerns, **no Web browser will provide** any specific information about the actual network your users' device is connected to, like **the SSID on a Wi-Fi, or the MAC address of their device.** - -## Contributions and Testing - -Tests are crucial to contributions to this package. All new contributions should be reasonably tested. - -In order to run tests in this package, do: - -``` -cd test -flutter run -d chrome -``` - -All contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md) guide to get started. - -## Issues and feedback - -Please file an [issue](https://github.com/ditman/plugins/issues/new) -to send feedback or report a bug. - -**Thank you!** diff --git a/packages/connectivity/connectivity_for_web/example/README.md b/packages/connectivity/connectivity_for_web/example/README.md deleted file mode 100644 index 8a6e74b107ea..000000000000 --- a/packages/connectivity/connectivity_for_web/example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Testing - -This package uses `package:integration_test` to run its tests in a web browser. - -See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) -in the Flutter wiki for instructions to setup and run the tests in this package. - -Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) -for more info. \ No newline at end of file diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart b/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart deleted file mode 100644 index e6faa30da4dc..000000000000 --- a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:connectivity_for_web/src/network_information_api_connectivity_plugin.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'src/connectivity_mocks.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('checkConnectivity', () { - void testCheckConnectivity({ - String? type, - String? effectiveType, - num? downlink = 10, - int? rtt = 50, - required ConnectivityResult expected, - }) { - final connection = FakeNetworkInformation( - type: type, - effectiveType: effectiveType, - downlink: downlink, - rtt: rtt, - ); - - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - expect(plugin.checkConnectivity(), completion(equals(expected))); - } - - testWidgets('0 downlink and rtt -> none', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '4g', - downlink: 0, - rtt: 0, - expected: ConnectivityResult.none); - }); - testWidgets('slow-2g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: 'slow-2g', expected: ConnectivityResult.mobile); - }); - testWidgets('2g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '2g', expected: ConnectivityResult.mobile); - }); - testWidgets('3g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '3g', expected: ConnectivityResult.mobile); - }); - testWidgets('4g -> wifi', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '4g', expected: ConnectivityResult.wifi); - }); - }); - - group('get onConnectivityChanged', () { - testWidgets('puts change events in a Stream', (WidgetTester tester) async { - final connection = FakeNetworkInformation(); - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - - // The onConnectivityChanged stream is infinite, so we only .take(2) so the test completes. - // We need to do .toList() now, because otherwise the Stream won't be actually listened to, - // and we'll miss the calls to mockChangeValue below. - final results = plugin.onConnectivityChanged.take(2).toList(); - - // Fake a disconnect-reconnect - await connection.mockChangeValue(downlink: 0, rtt: 0); - await connection.mockChangeValue( - downlink: 10, rtt: 50, effectiveType: '4g'); - - // Expect to see the disconnect-reconnect in the resulting stream. - expect( - results, - completion([ConnectivityResult.none, ConnectivityResult.wifi]), - ); - }); - }); -} diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart b/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart deleted file mode 100644 index 556b6fe6fca0..000000000000 --- a/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:html'; -import 'dart:js_util' show getProperty; - -import 'package:flutter_test/flutter_test.dart'; - -/// A Fake implementation of the NetworkInformation API that allows -/// for external modification of its values. -/// -/// Note that the DOM API works by internally mutating and broadcasting -/// 'change' events. -class FakeNetworkInformation extends Fake implements NetworkInformation { - String? _type; - String? _effectiveType; - num? _downlink; - int? _rtt; - - @override - String? get type => _type; - - @override - String? get effectiveType => _effectiveType; - - @override - num? get downlink => _downlink; - - @override - int? get rtt => _rtt; - - FakeNetworkInformation({ - String? type, - String? effectiveType, - num? downlink, - int? rtt, - }) : this._type = type, - this._effectiveType = effectiveType, - this._downlink = downlink, - this._rtt = rtt; - - /// Changes the desired values, and triggers the change event listener. - Future mockChangeValue({ - String? type, - String? effectiveType, - num? downlink, - int? rtt, - }) async { - this._type = type; - this._effectiveType = effectiveType; - this._downlink = downlink; - this._rtt = rtt; - - // This is set by the onConnectivityChanged getter... - final Function onchange = getProperty(this, 'onchange') as Function; - onchange(Event('change')); - } -} diff --git a/packages/connectivity/connectivity_for_web/example/lib/main.dart b/packages/connectivity/connectivity_for_web/example/lib/main.dart deleted file mode 100644 index e1a38dcdcd46..000000000000 --- a/packages/connectivity/connectivity_for_web/example/lib/main.dart +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -void main() { - runApp(MyApp()); -} - -/// App for testing -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: TextDirection.ltr, - child: Text('Testing... Look at the console output for results!'), - ); - } -} diff --git a/packages/connectivity/connectivity_for_web/example/pubspec.yaml b/packages/connectivity/connectivity_for_web/example/pubspec.yaml deleted file mode 100644 index 3b8e209e2486..000000000000 --- a/packages/connectivity/connectivity_for_web/example/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: connectivity_for_web_integration_tests -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" - -dependencies: - connectivity_for_web: - path: ../ - flutter: - sdk: flutter - -dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter diff --git a/packages/connectivity/connectivity_for_web/example/run_test.sh b/packages/connectivity/connectivity_for_web/example/run_test.sh deleted file mode 100755 index aa52974f310e..000000000000 --- a/packages/connectivity/connectivity_for_web/example/run_test.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/bash -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -if pgrep -lf chromedriver > /dev/null; then - echo "chromedriver is running." - - if [ $# -eq 0 ]; then - echo "No target specified, running all tests..." - find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' - else - echo "Running test target: $1..." - set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 - fi - - else - echo "chromedriver is not running." - echo "Please, check the README.md for instructions on how to use run_test.sh" -fi - diff --git a/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart b/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart deleted file mode 100644 index 4f10f2a522f3..000000000000 --- a/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/connectivity/connectivity_for_web/example/web/index.html b/packages/connectivity/connectivity_for_web/example/web/index.html deleted file mode 100644 index 7fb138cc90fa..000000000000 --- a/packages/connectivity/connectivity_for_web/example/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - example - - - - - diff --git a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart deleted file mode 100644 index d1c6811f5349..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -import 'src/network_information_api_connectivity_plugin.dart'; -import 'src/dart_html_connectivity_plugin.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class ConnectivityPlugin extends ConnectivityPlatform { - /// Factory method that initializes the connectivity plugin platform with an instance - /// of the plugin for the web. - static void registerWith(Registrar registrar) { - if (NetworkInformationApiConnectivityPlugin.isSupported()) { - ConnectivityPlatform.instance = NetworkInformationApiConnectivityPlugin(); - } else { - ConnectivityPlatform.instance = DartHtmlConnectivityPlugin(); - } - } - - // The following are completely unsupported methods on the web platform. - - // Creates an "unsupported_operation" PlatformException for a given `method` name. - Object _unsupported(String method) { - return PlatformException( - code: 'UNSUPPORTED_OPERATION', - message: '$method() is not supported on the web platform.', - ); - } - - /// Obtains the wifi name (SSID) of the connected network - @override - Future getWifiName() { - throw _unsupported('getWifiName'); - } - - /// Obtains the wifi BSSID of the connected network. - @override - Future getWifiBSSID() { - throw _unsupported('getWifiBSSID'); - } - - /// Obtains the IP address of the connected wifi network - @override - Future getWifiIP() { - throw _unsupported('getWifiIP'); - } - - /// Request to authorize the location service (Only on iOS). - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - throw _unsupported('requestLocationServiceAuthorization'); - } - - /// Get the current location service authorization (Only on iOS). - @override - Future getLocationServiceAuthorization() { - throw _unsupported('getLocationServiceAuthorization'); - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart deleted file mode 100644 index 475ec0d675b7..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:html' as html show window; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:connectivity_for_web/connectivity_for_web.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class DartHtmlConnectivityPlugin extends ConnectivityPlugin { - /// Checks the connection status of the device. - @override - Future checkConnectivity() async { - return html.window.navigator.onLine ?? false - ? ConnectivityResult.wifi - : ConnectivityResult.none; - } - - StreamController? _connectivityResult; - - /// Returns a Stream of ConnectivityResults changes. - @override - Stream get onConnectivityChanged { - if (_connectivityResult == null) { - _connectivityResult = StreamController.broadcast(); - // Fallback to dart:html window.onOnline / window.onOffline - html.window.onOnline.listen((event) { - _connectivityResult!.add(ConnectivityResult.wifi); - }); - html.window.onOffline.listen((event) { - _connectivityResult!.add(ConnectivityResult.none); - }); - } - return _connectivityResult!.stream; - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart deleted file mode 100644 index 6554f7a8c124..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:html' as html show window, NetworkInformation; -import 'dart:js' show allowInterop; -import 'dart:js_util' show setProperty; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:connectivity_for_web/connectivity_for_web.dart'; -import 'package:flutter/foundation.dart'; - -import 'utils/connectivity_result.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { - final html.NetworkInformation _networkInformation; - - /// A check to determine if this version of the plugin can be used. - static bool isSupported() => html.window.navigator.connection != null; - - /// The constructor of the plugin. - NetworkInformationApiConnectivityPlugin() - : this.withConnection(html.window.navigator.connection!); - - /// Creates the plugin, with an override of the NetworkInformation object. - @visibleForTesting - NetworkInformationApiConnectivityPlugin.withConnection( - html.NetworkInformation connection) - : _networkInformation = connection; - - /// Checks the connection status of the device. - @override - Future checkConnectivity() async { - return networkInformationToConnectivityResult(_networkInformation); - } - - StreamController? _connectivityResultStreamController; - late Stream _connectivityResultStream; - - /// Returns a Stream of ConnectivityResults changes. - @override - Stream get onConnectivityChanged { - if (_connectivityResultStreamController == null) { - _connectivityResultStreamController = - StreamController(); - - // Directly write the 'onchange' function on the networkInformation object. - setProperty(_networkInformation, 'onchange', allowInterop((_) { - _connectivityResultStreamController! - .add(networkInformationToConnectivityResult(_networkInformation)); - })); - // TODO: Implement the above with _networkInformation.onChange: - // _networkInformation.onChange.listen((_) { - // _connectivityResult - // .add(networkInformationToConnectivityResult(_networkInformation)); - // }); - // Once we can detect when to *cancel* a subscription to the _networkInformation - // onChange Stream upon hot restart. - // https://github.com/dart-lang/sdk/issues/42679 - _connectivityResultStream = - _connectivityResultStreamController!.stream.asBroadcastStream(); - } - return _connectivityResultStream; - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart b/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart deleted file mode 100644 index 691bd6da3bfb..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html show NetworkInformation; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -/// Converts an incoming NetworkInformation object into the correct ConnectivityResult. -ConnectivityResult networkInformationToConnectivityResult( - html.NetworkInformation? info, -) { - if (info == null) { - return ConnectivityResult.none; - } - if (info.downlink == 0 && info.rtt == 0) { - return ConnectivityResult.none; - } - if (info.effectiveType != null) { - return _effectiveTypeToConnectivityResult(info.effectiveType!); - } - if (info.type != null) { - return _typeToConnectivityResult(info.type!); - } - return ConnectivityResult.none; -} - -ConnectivityResult _effectiveTypeToConnectivityResult(String effectiveType) { - // Possible values: - /*'2g'|'3g'|'4g'|'slow-2g'*/ - switch (effectiveType) { - case 'slow-2g': - case '2g': - case '3g': - return ConnectivityResult.mobile; - default: - return ConnectivityResult.wifi; - } -} - -ConnectivityResult _typeToConnectivityResult(String type) { - // Possible values: - /*'bluetooth'|'cellular'|'ethernet'|'mixed'|'none'|'other'|'unknown'|'wifi'|'wimax'*/ - switch (type) { - case 'none': - return ConnectivityResult.none; - case 'bluetooth': - case 'cellular': - case 'mixed': - case 'other': - case 'unknown': - return ConnectivityResult.mobile; - default: - return ConnectivityResult.wifi; - } -} diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml deleted file mode 100644 index 2aaa8bd978fa..000000000000 --- a/packages/connectivity/connectivity_for_web/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: connectivity_for_web -description: An implementation for the web platform of the Flutter `connectivity` plugin. This uses the NetworkInformation Web API, with a fallback to Navigator.onLine. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.4.0+1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - implements: connectivity - platforms: - web: - pluginClass: ConnectivityPlugin - fileName: connectivity_for_web.dart - -dependencies: - connectivity_platform_interface: ^2.0.0 - flutter_web_plugins: - sdk: flutter - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/packages/connectivity/connectivity_for_web/test/README.md b/packages/connectivity/connectivity_for_web/test/README.md deleted file mode 100644 index 7c5b4ad682ba..000000000000 --- a/packages/connectivity/connectivity_for_web/test/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## test - -This package uses integration tests for testing. - -See `example/README.md` for more info. diff --git a/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart b/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart deleted file mode 100644 index 442c50144727..000000000000 --- a/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('Tell the user where to find the real tests', () { - print('---'); - print('This package uses integration_test for its tests.'); - print('See `example/README.md` for more info.'); - print('---'); - }); -} diff --git a/packages/connectivity/connectivity_macos/AUTHORS b/packages/connectivity/connectivity_macos/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/connectivity/connectivity_macos/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md deleted file mode 100644 index 46a4038f91ee..000000000000 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ /dev/null @@ -1,69 +0,0 @@ -## 0.2.1+2 - -* Add Swift language version to podspec. -* Fix `implements` package name in pubspec. - -## 0.2.1+1 - -* Ignore Reachability pointer to int cast warning. - -## 0.2.1 - -* Add `implements` to pubspec.yaml. - -## 0.2.0 - -* Remove placeholder Dart file. -* Update Dart SDK constraint for compatibility with null safety. - -## 0.1.0+8 - -* Update Flutter SDK constraint. - -## 0.1.0+7 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.1.0+6 - -* Update license headers. - -## 0.1.0+5 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. -* Remove no-op android folder in the example app. - -## 0.1.0+4 - -* Remove Android folder from `connectivity_macos`. - -## 0.1.0+3 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. -* Fix CocoaPods podspec lint warnings. -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.1.0+2 - -* Remove hard coded ios workspace setting of the example app. - -## 0.1.0+1 - -* Make the pedantic dev_dependency explicit. - -## 0.1.0 - -* Adds an example app to trigger CI tests. Bumped the MINOR version to -avoid compatibility issues once this packages is endorsed. - -## 0.0.2+1 - -* Add CHANGELOG. - -## 0.0.1 - -* Initial open source release. diff --git a/packages/connectivity/connectivity_macos/LICENSE b/packages/connectivity/connectivity_macos/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/connectivity/connectivity_macos/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/connectivity/connectivity_macos/README.md b/packages/connectivity/connectivity_macos/README.md deleted file mode 100644 index 6974fd1fcc7e..000000000000 --- a/packages/connectivity/connectivity_macos/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# connectivity_macos - -The macos implementation of [`connectivity`]. - -## Usage - -### Import the package - - -This package has been endorsed, meaning that you only need to add `connectivity` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:connectivity`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - connectivity: ^0.4.8 - ... -``` - -Refer to the `connectivity` [documentation](https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity) for more details. diff --git a/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart b/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart deleted file mode 100644 index 3e2b1c008a84..000000000000 --- a/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Connectivity test driver', () { - late ConnectivityPlatform _connectivity; - - setUpAll(() async { - _connectivity = ConnectivityPlatform.instance; - }); - - testWidgets('test connectivity result', (WidgetTester tester) async { - final ConnectivityResult result = await _connectivity.checkConnectivity(); - expect(result, isNotNull); - switch (result) { - case ConnectivityResult.wifi: - expect(_connectivity.getWifiName(), completes); - expect(_connectivity.getWifiBSSID(), completes); - expect((await _connectivity.getWifiIP()), isNotNull); - break; - default: - break; - } - }); - - testWidgets('test location methods, iOS only', (WidgetTester tester) async { - if (Platform.isIOS) { - expect((await _connectivity.getLocationServiceAuthorization()), - LocationAuthorizationStatus.notDetermined); - } - }); - }); -} diff --git a/packages/connectivity/connectivity_macos/example/lib/main.dart b/packages/connectivity/connectivity_macos/example/lib/main.dart deleted file mode 100644 index d0e07341fe92..000000000000 --- a/packages/connectivity/connectivity_macos/example/lib/main.dart +++ /dev/null @@ -1,172 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// Sets a platform override for desktop to avoid exceptions. See -// https://flutter.dev/desktop#target-platform-override for more info. -void _enablePlatformOverrideForDesktop() { - if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - } -} - -void main() { - _enablePlatformOverrideForDesktop(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, this.title}) : super(key: key); - - final String? title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _connectionStatus = 'Unknown'; - final ConnectivityPlatform _connectivity = ConnectivityPlatform.instance; - late StreamSubscription _connectivitySubscription; - - @override - void initState() { - super.initState(); - initConnectivity(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); - } - - @override - void dispose() { - _connectivitySubscription.cancel(); - super.dispose(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - late ConnectivityResult result; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { - print(e.toString()); - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) { - return Future.value(null); - } - - return _updateConnectionStatus(result); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center(child: Text('Connection Status: $_connectionStatus')), - ); - } - - Future _updateConnectionStatus(ConnectivityResult result) async { - switch (result) { - case ConnectivityResult.wifi: - String? wifiName, wifiBSSID, wifiIP; - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _connectivity.getWifiName(); - } else { - wifiName = await _connectivity.getWifiName(); - } - } else { - wifiName = await _connectivity.getWifiName(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiName = "Failed to get Wifi Name"; - } - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _connectivity.getWifiBSSID(); - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiBSSID = "Failed to get Wifi BSSID"; - } - - try { - wifiIP = await _connectivity.getWifiIP(); - } on PlatformException catch (e) { - print(e.toString()); - wifiIP = "Failed to get Wifi IP"; - } - - setState(() { - _connectionStatus = '$result\n' - 'Wifi Name: $wifiName\n' - 'Wifi BSSID: $wifiBSSID\n' - 'Wifi IP: $wifiIP\n'; - }); - break; - case ConnectivityResult.mobile: - case ConnectivityResult.none: - setState(() => _connectionStatus = result.toString()); - break; - default: - setState(() => _connectionStatus = 'Failed to get connectivity.'); - break; - } - } -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Podfile b/packages/connectivity/connectivity_macos/example/macos/Podfile deleted file mode 100644 index e9131e75c4d2..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Podfile +++ /dev/null @@ -1,46 +0,0 @@ -platform :osx, '10.11' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - # Work around https://github.com/flutter/flutter/issues/82964. - if target.name == 'Reachability' - target.build_configurations.each do |config| - config.build_settings['WARNING_CFLAGS'] = '-Wno-pointer-to-int-cast' - end - end - flutter_additional_macos_build_settings(target) - end -end diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 0e2413493f6e..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,654 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748ADDF1719804343BB18004 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* connectivity_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = connectivity_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 748ADDF1719804343BB18004 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - D42EAEE5849744148CC78D83 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* connectivity_example.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D42EAEE5849744148CC78D83 /* Pods */ = { - isa = PBXGroup; - children = ( - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */, - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */, - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 748ADDF1719804343BB18004 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* connectivity_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Google LLC"; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; - }; - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 2a7d3e7f34ac..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift b/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index 5cec4c48f620..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f11..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46993 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 diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3276 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 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136a47ed24125c7efc79d68a4a01961b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14800 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index db9bebac4b66..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = connectivity_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464f4..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561c8..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780b1..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements b/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Info.plist b/packages/connectivity/connectivity_macos/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a443..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 32aaeedceb1f..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements b/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/connectivity/connectivity_macos/example/pubspec.yaml b/packages/connectivity/connectivity_macos/example/pubspec.yaml deleted file mode 100644 index 0af2e1587b00..000000000000 --- a/packages/connectivity/connectivity_macos/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: connectivity_example -description: Demonstrates how to use the connectivity plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" - -dependencies: - flutter: - sdk: flutter - connectivity_platform_interface: ^2.0.0 - connectivity_macos: - # When depending on this package from a real application you should use: - # connectivity_macos: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart deleted file mode 100644 index 4f10f2a522f3..000000000000 --- a/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift b/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift deleted file mode 100644 index 69efe80df5ac..000000000000 --- a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import CoreWLAN -import FlutterMacOS -import Reachability -import SystemConfiguration.CaptiveNetwork - -public class ConnectivityPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { - var reach: Reachability? - var eventSink: FlutterEventSink? - var cwinterface: CWInterface? - - public override init() { - cwinterface = CWWiFiClient.shared().interface() - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/connectivity", - binaryMessenger: registrar.messenger) - - let streamChannel = FlutterEventChannel( - name: "plugins.flutter.io/connectivity_status", - binaryMessenger: registrar.messenger) - - let instance = ConnectivityPlugin() - streamChannel.setStreamHandler(instance) - - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "check": - result(statusFromReachability(reachability: Reachability.forInternetConnection())) - case "wifiName": - result(cwinterface?.ssid()) - case "wifiBSSID": - result(cwinterface?.bssid()) - case "wifiIPAddress": - result(getWifiIP()) - default: - result(FlutterMethodNotImplemented) - } - } - - /// Returns a string describing connection type - /// - /// - Parameters: - /// - reachability: an instance of reachability - /// - Returns: connection type string - private func statusFromReachability(reachability: Reachability?) -> String { - // checks any non-WWAN connection - if reachability?.isReachableViaWiFi() ?? false { - return "wifi" - } - - return "none" - } - - public func onListen( - withArguments _: Any?, - eventSink events: @escaping FlutterEventSink - ) -> FlutterError? { - reach = Reachability.forInternetConnection() - eventSink = events - - NotificationCenter.default.addObserver( - self, - selector: #selector(reachabilityChanged), - name: NSNotification.Name.reachabilityChanged, - object: reach) - - reach?.startNotifier() - - return nil - } - - @objc private func reachabilityChanged(notification: NSNotification) { - let reach = notification.object - let reachability = statusFromReachability(reachability: reach as? Reachability) - eventSink?(reachability) - } - - public func onCancel(withArguments _: Any?) -> FlutterError? { - reach?.stopNotifier() - NotificationCenter.default.removeObserver(self) - eventSink = nil - return nil - } -} diff --git a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h deleted file mode 100644 index e5370fb349c3..000000000000 --- a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -#import "SystemConfiguration/CaptiveNetwork.h" - -#include -#include - -NSString* getWifiIP() { - NSString* address = @"error"; - struct ifaddrs* interfaces = NULL; - struct ifaddrs* temp_addr = NULL; - int success = 0; - - // Retrieve the current interfaces - returns 0 on success. - success = getifaddrs(&interfaces); - if (success == 0) { - // Loop through linked list of interfaces. - temp_addr = interfaces; - while (temp_addr != NULL) { - if (temp_addr->ifa_addr->sa_family == AF_INET) { - // Check if interface is en0 which is the wifi connection on the iPhone. - if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { - // Get NSString from C String - address = [NSString - stringWithUTF8String:inet_ntoa(((struct sockaddr_in*)temp_addr->ifa_addr)->sin_addr)]; - } - } - - temp_addr = temp_addr->ifa_next; - } - } - - // Free memory - freeifaddrs(interfaces); - - return address; -} diff --git a/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec b/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec deleted file mode 100644 index 51629084a23d..000000000000 --- a/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity_macos' - s.version = '0.0.1' - s.summary = 'Flutter plugin for checking connectivity' - s.description = <<-DESC - Desktop implementation of the connectivity plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - s.dependency 'Reachability' - - s.platform = :osx - s.osx.deployment_target = '10.11' - s.swift_version = '5.0' -end diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml deleted file mode 100644 index b98f23d34eb7..000000000000 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: connectivity_macos -description: macOS implementation of the connectivity plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.2.1+2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - implements: connectivity - platforms: - macos: - pluginClass: ConnectivityPlugin - -dependencies: - flutter: - sdk: flutter - # The implementation of this plugin doesn't explicitly depend on the method channel - # defined in the platform interface. - # To prevent potential breakages, this dependency is added. - # - # In the future, this plugin's platform code should be able to reference the - # interface's platform code. (Android already supports this). - connectivity_platform_interface: ^2.0.0 - -dev_dependencies: - pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/AUTHORS b/packages/connectivity/connectivity_platform_interface/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/connectivity/connectivity_platform_interface/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md deleted file mode 100644 index ee75d03ccc65..000000000000 --- a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,44 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.7 - -* Update Flutter SDK constraint. - -## 1.0.6 - -* Update lower bound of dart dependency to 2.1.0. - -## 1.0.5 - -* Remove dart:io Platform checks from the MethodChannel implementation. This is -tripping the analysis of other versions of the plugin. - -## 1.0.4 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. - -## 1.0.3 - -* Make the pedantic dev_dependency explicit. - -## 1.0.2 - -* Bring ConnectivityResult and LocationAuthorizationStatus enums from the core package. -* Use the above Enums as return values for ConnectivityPlatformInterface methods. -* Modify the MethodChannel implementation so it returns the right types. -* Bring all utility methods, asserts and other logic that is only needed on the MethodChannel implementation from the core package. -* Bring MethodChannel unit tests from core package. - -## 1.0.1 - -* Fix README.md link. - -## 1.0.0 - -* Initial release. diff --git a/packages/connectivity/connectivity_platform_interface/LICENSE b/packages/connectivity/connectivity_platform_interface/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/connectivity/connectivity_platform_interface/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/connectivity/connectivity_platform_interface/README.md b/packages/connectivity/connectivity_platform_interface/README.md deleted file mode 100644 index 76b39315637e..000000000000 --- a/packages/connectivity/connectivity_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# connectivity_platform_interface - -A common platform interface for the [`connectivity`][1] plugin. - -This interface allows platform-specific implementations of the `connectivity` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `connectivity`, extend -[`ConnectivityPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`ConnectivityPlatform` by calling -`ConnectivityPlatform.instance = MyPlatformConnectivity()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../ -[2]: lib/connectivity_platform_interface.dart diff --git a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart deleted file mode 100644 index 6667b353a4aa..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'src/enums.dart'; -import 'src/method_channel_connectivity.dart'; - -export 'src/enums.dart'; - -/// The interface that implementations of connectivity must implement. -/// -/// Platform implementations should extend this class rather than implement it as `Connectivity` -/// does not consider newly added methods to be breaking changes. Extending this class -/// (using `extends`) ensures that the subclass will get the default implementation, while -/// platform implementations that `implements` this interface will be broken by newly added -/// [ConnectivityPlatform] methods. -abstract class ConnectivityPlatform extends PlatformInterface { - /// Constructs a ConnectivityPlatform. - ConnectivityPlatform() : super(token: _token); - - static final Object _token = Object(); - - static ConnectivityPlatform _instance = MethodChannelConnectivity(); - - /// The default instance of [ConnectivityPlatform] to use. - /// - /// Defaults to [MethodChannelConnectivity]. - static ConnectivityPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [ConnectivityPlatform] when they register themselves. - static set instance(ConnectivityPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Checks the connection status of the device. - Future checkConnectivity() { - throw UnimplementedError('checkConnectivity() has not been implemented.'); - } - - /// Returns a Stream of ConnectivityResults changes. - Stream get onConnectivityChanged { - throw UnimplementedError( - 'get onConnectivityChanged has not been implemented.'); - } - - /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { - throw UnimplementedError('getWifiName() has not been implemented.'); - } - - /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { - throw UnimplementedError('getWifiBSSID() has not been implemented.'); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - throw UnimplementedError('getWifiIP() has not been implemented.'); - } - - /// Request to authorize the location service (Only on iOS). - Future requestLocationServiceAuthorization( - {bool requestAlwaysLocationUsage = false}) { - throw UnimplementedError( - 'requestLocationServiceAuthorization() has not been implemented.'); - } - - /// Get the current location service authorization (Only on iOS). - Future getLocationServiceAuthorization() { - throw UnimplementedError( - 'getLocationServiceAuthorization() has not been implemented.'); - } -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart b/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart deleted file mode 100644 index b77f54cf60b2..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart +++ /dev/null @@ -1,36 +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. - -/// Connection status check result. -enum ConnectivityResult { - /// WiFi: Device connected via Wi-Fi - wifi, - - /// Mobile: Device connected to cellular network - mobile, - - /// None: Device not connected to any network - none -} - -/// The status of the location service authorization. -enum LocationAuthorizationStatus { - /// The authorization of the location service is not determined. - notDetermined, - - /// This app is not authorized to use location. - restricted, - - /// User explicitly denied the location service. - denied, - - /// User authorized the app to access the location at any time. - authorizedAlways, - - /// User authorized the app to access the location when the app is visible to them. - authorizedWhenInUse, - - /// Status unknown. - unknown -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart deleted file mode 100644 index bdf820ac3ba7..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; - -import 'utils.dart'; - -/// An implementation of [ConnectivityPlatform] that uses method channels. -class MethodChannelConnectivity extends ConnectivityPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - MethodChannel('plugins.flutter.io/connectivity'); - - /// The event channel used to receive ConnectivityResult changes from the native platform. - @visibleForTesting - EventChannel eventChannel = - EventChannel('plugins.flutter.io/connectivity_status'); - - Stream? _onConnectivityChanged; - - /// Fires whenever the connectivity state changes. - Stream get onConnectivityChanged { - if (_onConnectivityChanged == null) { - _onConnectivityChanged = - eventChannel.receiveBroadcastStream().map((dynamic result) { - return result != null ? result.toString() : ''; - }).map(parseConnectivityResult); - } - return _onConnectivityChanged!; - } - - @override - Future checkConnectivity() async { - final String checkResult = - await methodChannel.invokeMethod('check') ?? ''; - return parseConnectivityResult(checkResult); - } - - @override - Future getWifiName() async { - String? wifiName = await methodChannel.invokeMethod('wifiName'); - // as Android might return , uniforming result - // our iOS implementation will return null - if (wifiName == '') { - wifiName = null; - } - return wifiName; - } - - @override - Future getWifiBSSID() { - return methodChannel.invokeMethod('wifiBSSID'); - } - - @override - Future getWifiIP() { - return methodChannel.invokeMethod('wifiIPAddress'); - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) async { - final String requestLocationServiceResult = await methodChannel - .invokeMethod('requestLocationServiceAuthorization', - [requestAlwaysLocationUsage]) ?? - ''; - return parseLocationAuthorizationStatus(requestLocationServiceResult); - } - - @override - Future getLocationServiceAuthorization() async { - final String getLocationServiceResult = await methodChannel - .invokeMethod('getLocationServiceAuthorization') ?? - ''; - return parseLocationAuthorizationStatus(getLocationServiceResult); - } -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart b/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart deleted file mode 100644 index 3b5b753f6c29..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -/// Convert a String to a ConnectivityResult value. -ConnectivityResult parseConnectivityResult(String state) { - switch (state) { - case 'wifi': - return ConnectivityResult.wifi; - case 'mobile': - return ConnectivityResult.mobile; - case 'none': - default: - return ConnectivityResult.none; - } -} - -/// Convert a String to a LocationAuthorizationStatus value. -LocationAuthorizationStatus parseLocationAuthorizationStatus(String result) { - switch (result) { - case 'notDetermined': - return LocationAuthorizationStatus.notDetermined; - case 'restricted': - return LocationAuthorizationStatus.restricted; - case 'denied': - return LocationAuthorizationStatus.denied; - case 'authorizedAlways': - return LocationAuthorizationStatus.authorizedAlways; - case 'authorizedWhenInUse': - return LocationAuthorizationStatus.authorizedWhenInUse; - default: - return LocationAuthorizationStatus.unknown; - } -} diff --git a/packages/connectivity/connectivity_platform_interface/pubspec.yaml b/packages/connectivity/connectivity_platform_interface/pubspec.yaml deleted file mode 100644 index 2003fdde6eeb..000000000000 --- a/packages/connectivity/connectivity_platform_interface/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: connectivity_platform_interface -description: A common platform interface for the connectivity plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -# NOTE: We strongly prefer non-breaking changes, even at the expense of a -# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart b/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart deleted file mode 100644 index b69feae252eb..000000000000 --- a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.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. - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity_platform_interface/src/method_channel_connectivity.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelConnectivity', () { - final List log = []; - late MethodChannelConnectivity methodChannelConnectivity; - - setUp(() async { - methodChannelConnectivity = MethodChannelConnectivity(); - - methodChannelConnectivity.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'check': - return 'wifi'; - case 'wifiName': - return '1337wifi'; - case 'wifiBSSID': - return 'c0:ff:33:c0:d3:55'; - case 'wifiIPAddress': - return '127.0.0.1'; - case 'requestLocationServiceAuthorization': - return 'authorizedAlways'; - case 'getLocationServiceAuthorization': - return 'authorizedAlways'; - default: - return null; - } - }); - log.clear(); - MethodChannel(methodChannelConnectivity.eventChannel.name) - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'listen': - await _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - methodChannelConnectivity.eventChannel.name, - methodChannelConnectivity.eventChannel.codec - .encodeSuccessEnvelope('wifi'), - (_) {}, - ); - break; - case 'cancel': - default: - return null; - } - }); - }); - - test('onConnectivityChanged', () async { - final ConnectivityResult result = - await methodChannelConnectivity.onConnectivityChanged.first; - expect(result, ConnectivityResult.wifi); - }); - - test('getWifiName', () async { - final String? result = await methodChannelConnectivity.getWifiName(); - expect(result, '1337wifi'); - expect( - log, - [ - isMethodCall( - 'wifiName', - arguments: null, - ), - ], - ); - }); - - test('getWifiBSSID', () async { - final String? result = await methodChannelConnectivity.getWifiBSSID(); - expect(result, 'c0:ff:33:c0:d3:55'); - expect( - log, - [ - isMethodCall( - 'wifiBSSID', - arguments: null, - ), - ], - ); - }); - - test('getWifiIP', () async { - final String? result = await methodChannelConnectivity.getWifiIP(); - expect(result, '127.0.0.1'); - expect( - log, - [ - isMethodCall( - 'wifiIPAddress', - arguments: null, - ), - ], - ); - }); - - test('requestLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelConnectivity.requestLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'requestLocationServiceAuthorization', - arguments: [false], - ), - ], - ); - }); - - test('getLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelConnectivity.getLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'getLocationServiceAuthorization', - arguments: null, - ), - ], - ); - }); - - test('checkConnectivity', () async { - final ConnectivityResult result = - await methodChannelConnectivity.checkConnectivity(); - expect(result, ConnectivityResult.wifi); - expect( - log, - [ - isMethodCall( - 'check', - arguments: null, - ), - ], - ); - }); - }); -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/device_info/analysis_options.yaml b/packages/device_info/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/device_info/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/device_info/device_info/AUTHORS b/packages/device_info/device_info/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/device_info/device_info/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md deleted file mode 100644 index 825b12d8c7ad..000000000000 --- a/packages/device_info/device_info/CHANGELOG.md +++ /dev/null @@ -1,186 +0,0 @@ -## NEXT - -* Updates compileSdkVersion to 31. - -## 2.0.3 - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. -* Started using Background Platform Channels when available. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 1.0.1 - -* Update Flutter SDK constraint. - -## 1.0.0 - -* Announce 1.0.0. - -## 0.4.2+10 - -* Update Dart SDK constraint in example. - -## 0.4.2+9 - -* Update android compileSdkVersion to 29. - -## 0.4.2+8 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.2+7 - -* Port device_info plugin to use platform interface. - -## 0.4.2+6 - -* Moved everything from device_info to device_info/device_info - -## 0.4.2+5 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.2+4 - -Update lower bound of dart dependency to 2.1.0. - -## 0.4.2+3 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.2+2 - -* Fix CocoaPods podspec lint warnings. - -## 0.4.2+1 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Remove deprecated API usage warning in AndroidIntentPlugin.java. -* Migrates the Android example to V2 embedding. -* Bumps AGP to 3.6.1. - -## 0.4.2 - -* Add systemFeatures to AndroidDeviceInfo. - -## 0.4.1+5 - -* Make the pedantic dev_dependency explicit. - -## 0.4.1+4 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.1+3 - -* Fix pedantic errors. Adds some missing documentation and fixes unawaited - futures in the tests. - -## 0.4.1+2 - -* Remove AndroidX warning. - -## 0.4.1+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.1 - -* Support the v2 Android embedding. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - - -## 0.4.0+4 - -* Define clang module for iOS. - -## 0.4.0+3 - -* Update and migrate iOS example project. - -## 0.4.0+2 - -* Bump minimum Flutter version to 1.5.0. -* Add missing template type parameter to `invokeMethod` calls. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.0 - -* Added ability to get Android ID for Android devices - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.2 - -* Fixed Dart 2 type errors. - -## 0.1.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.5 - -* Added FLT prefix to iOS types - -## 0.0.4 - -* Fixed Java/Dart communication error with empty lists - -## 0.0.3 - -* Added support for utsname - -## 0.0.2 - -* Fixed broken type comparison -* Added "isPhysicalDevice" field, detecting emulators/simulators - -## 0.0.1 - -* Implements platform-specific device/OS properties diff --git a/packages/device_info/device_info/LICENSE b/packages/device_info/device_info/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/device_info/device_info/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/device_info/device_info/README.md b/packages/device_info/device_info/README.md deleted file mode 100644 index 34cf9bb2ac2b..000000000000 --- a/packages/device_info/device_info/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# device_info - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`device_info_plus`](https://pub.dev/packages/device_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -Get current device information from within the Flutter application. - -# Usage - -Import `package:device_info/device_info.dart`, instantiate `DeviceInfoPlugin` -and use the Android and iOS getters to get platform-specific device -information. - -Example: - -```dart -import 'package:device_info/device_info.dart'; - -DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); -AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; -print('Running on ${androidInfo.model}'); // e.g. "Moto G (4)" - -IosDeviceInfo iosInfo = await deviceInfo.iosInfo; -print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" -``` - -You will find links to the API docs on the [pub page](https://pub.dev/packages/device_info). - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). - -For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle deleted file mode 100644 index 67fbf1fe5c65..000000000000 --- a/packages/device_info/device_info/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.deviceinfo' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 31 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/device_info/device_info/android/settings.gradle b/packages/device_info/device_info/android/settings.gradle deleted file mode 100644 index 0e75718c9a9d..000000000000 --- a/packages/device_info/device_info/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'device_info' diff --git a/packages/device_info/device_info/android/src/main/AndroidManifest.xml b/packages/device_info/device_info/android/src/main/AndroidManifest.xml deleted file mode 100644 index 03e76883266f..000000000000 --- a/packages/device_info/device_info/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java deleted file mode 100644 index 756f5e0ce820..000000000000 --- a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java +++ /dev/null @@ -1,67 +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.deviceinfo; - -import android.content.Context; -import android.util.Log; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodCodec; -import io.flutter.plugin.common.StandardMethodCodec; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; - -/** DeviceInfoPlugin */ -public class DeviceInfoPlugin implements FlutterPlugin { - static final String TAG = "DeviceInfoPlugin"; - MethodChannel channel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - DeviceInfoPlugin plugin = new DeviceInfoPlugin(); - plugin.setupMethodChannel(registrar.messenger(), registrar.context()); - } - - @Override - public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding binding) { - setupMethodChannel(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { - tearDownChannel(); - } - - private void setupMethodChannel(BinaryMessenger messenger, Context context) { - String channelName = "plugins.flutter.io/device_info"; - // TODO(gaaclarke): Remove reflection guard when https://github.com/flutter/engine/pull/29147 - // becomes available on the stable branch. - 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); - Log.d(TAG, "Use TaskQueues."); - } catch (Exception ex) { - channel = new MethodChannel(messenger, channelName); - Log.d(TAG, "Don't use TaskQueues."); - } - final MethodCallHandlerImpl handler = - new MethodCallHandlerImpl(context.getContentResolver(), context.getPackageManager()); - channel.setMethodCallHandler(handler); - } - - private void tearDownChannel() { - channel.setMethodCallHandler(null); - channel = null; - } -} diff --git a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java deleted file mode 100644 index 531e5db0c237..000000000000 --- a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java +++ /dev/null @@ -1,133 +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.deviceinfo; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.provider.Settings; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * The implementation of {@link MethodChannel.MethodCallHandler} for the plugin. Responsible for - * receiving method calls from method channel. - */ -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - - private final ContentResolver contentResolver; - private final PackageManager packageManager; - - /** Substitute for missing values. */ - private static final String[] EMPTY_STRING_LIST = new String[] {}; - - /** Constructs DeviceInfo. {@code contentResolver} and {@code packageManager} must not be null. */ - MethodCallHandlerImpl(ContentResolver contentResolver, PackageManager packageManager) { - this.contentResolver = contentResolver; - this.packageManager = packageManager; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (call.method.equals("getAndroidDeviceInfo")) { - Map build = new HashMap<>(); - build.put("board", Build.BOARD); - build.put("bootloader", Build.BOOTLOADER); - build.put("brand", Build.BRAND); - build.put("device", Build.DEVICE); - build.put("display", Build.DISPLAY); - build.put("fingerprint", Build.FINGERPRINT); - build.put("hardware", Build.HARDWARE); - build.put("host", Build.HOST); - build.put("id", Build.ID); - build.put("manufacturer", Build.MANUFACTURER); - build.put("model", Build.MODEL); - build.put("product", Build.PRODUCT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - build.put("supported32BitAbis", Arrays.asList(Build.SUPPORTED_32_BIT_ABIS)); - build.put("supported64BitAbis", Arrays.asList(Build.SUPPORTED_64_BIT_ABIS)); - build.put("supportedAbis", Arrays.asList(Build.SUPPORTED_ABIS)); - } else { - build.put("supported32BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supported64BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supportedAbis", Arrays.asList(EMPTY_STRING_LIST)); - } - build.put("tags", Build.TAGS); - build.put("type", Build.TYPE); - build.put("isPhysicalDevice", !isEmulator()); - build.put("androidId", getAndroidId()); - - build.put("systemFeatures", Arrays.asList(getSystemFeatures())); - - Map version = new HashMap<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - version.put("baseOS", Build.VERSION.BASE_OS); - version.put("previewSdkInt", Build.VERSION.PREVIEW_SDK_INT); - version.put("securityPatch", Build.VERSION.SECURITY_PATCH); - } - version.put("codename", Build.VERSION.CODENAME); - version.put("incremental", Build.VERSION.INCREMENTAL); - version.put("release", Build.VERSION.RELEASE); - version.put("sdkInt", Build.VERSION.SDK_INT); - build.put("version", version); - - result.success(build); - } else { - result.notImplemented(); - } - } - - private String[] getSystemFeatures() { - FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); - if (featureInfos == null) { - return EMPTY_STRING_LIST; - } - String[] features = new String[featureInfos.length]; - for (int i = 0; i < featureInfos.length; i++) { - features[i] = featureInfos[i].name; - } - return features; - } - - /** - * Returns the Android hardware device ID that is unique between the device + user and app - * signing. This key will change if the app is uninstalled or its data is cleared. Device factory - * reset will also result in a value change. - * - * @return The android ID - */ - @SuppressLint("HardwareIds") - private String getAndroidId() { - return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID); - } - - /** - * A simple emulator-detection based on the flutter tools detection logic and a couple of legacy - * detection systems - */ - private boolean isEmulator() { - return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); - } -} diff --git a/packages/device_info/device_info/example/README.md b/packages/device_info/device_info/example/README.md deleted file mode 100644 index ea47551011d0..000000000000 --- a/packages/device_info/device_info/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# device_info_example - -Demonstrates how to use the `device_info` plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/device_info/device_info/example/android/app/build.gradle b/packages/device_info/device_info/example/android/app/build.gradle deleted file mode 100644 index 9371207aba2d..000000000000 --- a/packages/device_info/device_info/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 31 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.deviceinfoexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 4268475986a3..000000000000 --- a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/device_info/device_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/device_info/device_info/example/android/build.gradle b/packages/device_info/device_info/example/android/build.gradle deleted file mode 100644 index 3274eb601b9b..000000000000 --- a/packages/device_info/device_info/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/device_info/device_info/example/android/gradle.properties b/packages/device_info/device_info/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/device_info/device_info/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c243e25a0fff..000000000000 --- a/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Mar 09 11:05:13 GMT 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/packages/device_info/device_info/example/android/settings.gradle b/packages/device_info/device_info/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/device_info/device_info/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/packages/device_info/device_info/example/integration_test/device_info_test.dart b/packages/device_info/device_info/example/integration_test/device_info_test.dart deleted file mode 100644 index 953eb856d62a..000000000000 --- a/packages/device_info/device_info/example/integration_test/device_info_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:device_info/device_info.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late IosDeviceInfo iosInfo; - late AndroidDeviceInfo androidInfo; - - setUpAll(() async { - final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - if (Platform.isIOS) { - iosInfo = await deviceInfoPlugin.iosInfo; - } else if (Platform.isAndroid) { - androidInfo = await deviceInfoPlugin.androidInfo; - } - }); - - testWidgets('Can get non-null device model', (WidgetTester tester) async { - if (Platform.isIOS) { - expect(iosInfo.model, isNotNull); - } else if (Platform.isAndroid) { - expect(androidInfo.model, isNotNull); - } - }); -} diff --git a/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist b/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig b/packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/device_info/device_info/example/ios/Flutter/Release.xcconfig b/packages/device_info/device_info/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/device_info/device_info/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/device_info/device_info/example/ios/Podfile b/packages/device_info/device_info/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/device_info/device_info/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index ca599db5b7ac..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 20A0DD43C00A880430740858 /* Pods */ = { - isa = PBXGroup; - children = ( - C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */, - E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 20A0DD43C00A880430740858 /* Pods */, - EA17DAB2B097E79A4CABE344 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - EA17DAB2B097E79A4CABE344 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6254f..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner/AppDelegate.h b/packages/device_info/device_info/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 0681d288bb70..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/AppDelegate.h +++ /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. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/device_info/device_info/example/ios/Runner/AppDelegate.m b/packages/device_info/device_info/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 30b87969f44a..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard b/packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner/Info.plist b/packages/device_info/device_info/example/ios/Runner/Info.plist deleted file mode 100644 index 1ea53f8dc54b..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - device_info_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/device_info/device_info/example/ios/Runner/main.m b/packages/device_info/device_info/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/device_info/device_info/example/lib/main.dart b/packages/device_info/device_info/example/lib/main.dart deleted file mode 100644 index 44e3fb4ee2f7..000000000000 --- a/packages/device_info/device_info/example/lib/main.dart +++ /dev/null @@ -1,146 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:device_info/device_info.dart'; - -void main() { - runZonedGuarded(() { - runApp(MyApp()); - }, (dynamic error, dynamic stack) { - print(error); - print(stack); - }); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - Map _deviceData = {}; - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - Future initPlatformState() async { - Map deviceData = {}; - - try { - if (Platform.isAndroid) { - deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo); - } else if (Platform.isIOS) { - deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo); - } - } on PlatformException { - deviceData = { - 'Error:': 'Failed to get platform version.' - }; - } - - if (!mounted) return; - - setState(() { - _deviceData = deviceData; - }); - } - - Map _readAndroidBuildData(AndroidDeviceInfo build) { - return { - 'version.securityPatch': build.version.securityPatch, - 'version.sdkInt': build.version.sdkInt, - 'version.release': build.version.release, - 'version.previewSdkInt': build.version.previewSdkInt, - 'version.incremental': build.version.incremental, - 'version.codename': build.version.codename, - 'version.baseOS': build.version.baseOS, - 'board': build.board, - 'bootloader': build.bootloader, - 'brand': build.brand, - 'device': build.device, - 'display': build.display, - 'fingerprint': build.fingerprint, - 'hardware': build.hardware, - 'host': build.host, - 'id': build.id, - 'manufacturer': build.manufacturer, - 'model': build.model, - 'product': build.product, - 'supported32BitAbis': build.supported32BitAbis, - 'supported64BitAbis': build.supported64BitAbis, - 'supportedAbis': build.supportedAbis, - 'tags': build.tags, - 'type': build.type, - 'isPhysicalDevice': build.isPhysicalDevice, - 'androidId': build.androidId, - 'systemFeatures': build.systemFeatures, - }; - } - - Map _readIosDeviceInfo(IosDeviceInfo data) { - return { - 'name': data.name, - 'systemName': data.systemName, - 'systemVersion': data.systemVersion, - 'model': data.model, - 'localizedModel': data.localizedModel, - 'identifierForVendor': data.identifierForVendor, - 'isPhysicalDevice': data.isPhysicalDevice, - 'utsname.sysname:': data.utsname.sysname, - 'utsname.nodename:': data.utsname.nodename, - 'utsname.release:': data.utsname.release, - 'utsname.version:': data.utsname.version, - 'utsname.machine:': data.utsname.machine, - }; - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: Text( - Platform.isAndroid ? 'Android Device Info' : 'iOS Device Info'), - ), - body: ListView( - children: _deviceData.keys.map((String property) { - return Row( - children: [ - Container( - padding: const EdgeInsets.all(10.0), - child: Text( - property, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), - child: Text( - '${_deviceData[property]}', - maxLines: 10, - overflow: TextOverflow.ellipsis, - ), - )), - ], - ); - }).toList(), - ), - ), - ); - } -} diff --git a/packages/device_info/device_info/example/pubspec.yaml b/packages/device_info/device_info/example/pubspec.yaml deleted file mode 100644 index c4e84f60de5e..000000000000 --- a/packages/device_info/device_info/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: device_info_example -description: Demonstrates how to use the device_info plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - device_info: - # When depending on this package from a real application you should use: - # device_info: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/device_info/device_info/example/test_driver/integration_test.dart b/packages/device_info/device_info/example/test_driver/integration_test.dart deleted file mode 100644 index 4f10f2a522f3..000000000000 --- a/packages/device_info/device_info/example/test_driver/integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/device_info/device_info/ios/Assets/.gitkeep b/packages/device_info/device_info/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h deleted file mode 100644 index 511b5b893fcb..000000000000 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTDeviceInfoPlugin : NSObject -@end diff --git a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m deleted file mode 100644 index 3d4d25ad9c6e..000000000000 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTDeviceInfoPlugin.h" -#import - -@implementation FLTDeviceInfoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/device_info" - binaryMessenger:[registrar messenger]]; - FLTDeviceInfoPlugin* instance = [[FLTDeviceInfoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([@"getIosDeviceInfo" isEqualToString:call.method]) { - UIDevice* device = [UIDevice currentDevice]; - struct utsname un; - uname(&un); - - result(@{ - @"name" : [device name], - @"systemName" : [device systemName], - @"systemVersion" : [device systemVersion], - @"model" : [device model], - @"localizedModel" : [device localizedModel], - @"identifierForVendor" : [[device identifierForVendor] UUIDString], - @"isPhysicalDevice" : [self isDevicePhysical], - @"utsname" : @{ - @"sysname" : @(un.sysname), - @"nodename" : @(un.nodename), - @"release" : @(un.release), - @"version" : @(un.version), - @"machine" : @(un.machine), - } - }); - } else { - result(FlutterMethodNotImplemented); - } -} - -// return value is false if code is run on a simulator -- (NSString*)isDevicePhysical { -#if TARGET_OS_SIMULATOR - NSString* isPhysicalDevice = @"false"; -#else - NSString* isPhysicalDevice = @"true"; -#endif - - return isPhysicalDevice; -} - -@end diff --git a/packages/device_info/device_info/ios/device_info.podspec b/packages/device_info/device_info/ios/device_info.podspec deleted file mode 100644 index 1d1baa4787f6..000000000000 --- a/packages/device_info/device_info/ios/device_info.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'device_info' - s.version = '0.0.1' - s.summary = 'Flutter Device Info' - s.description = <<-DESC -Get current device information from within the Flutter application. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/device_info' } - s.documentation_url = 'https://pub.dev/packages/device_info' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/device_info/device_info/lib/device_info.dart b/packages/device_info/device_info/lib/device_info.dart deleted file mode 100644 index 1153ac6f7da6..000000000000 --- a/packages/device_info/device_info/lib/device_info.dart +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:device_info_platform_interface/device_info_platform_interface.dart'; - -export 'package:device_info_platform_interface/device_info_platform_interface.dart' - show AndroidBuildVersion, AndroidDeviceInfo, IosDeviceInfo, IosUtsname; - -/// Provides device and operating system information. -class DeviceInfoPlugin { - /// No work is done when instantiating the plugin. It's safe to call this - /// repeatedly or in performance-sensitive blocks. - DeviceInfoPlugin(); - - /// This information does not change from call to call. Cache it. - AndroidDeviceInfo? _cachedAndroidDeviceInfo; - - /// Information derived from `android.os.Build`. - /// - /// See: https://developer.android.com/reference/android/os/Build.html - Future get androidInfo async => - _cachedAndroidDeviceInfo ??= - await DeviceInfoPlatform.instance.androidInfo(); - - /// This information does not change from call to call. Cache it. - IosDeviceInfo? _cachedIosDeviceInfo; - - /// Information derived from `UIDevice`. - /// - /// See: https://developer.apple.com/documentation/uikit/uidevice - Future get iosInfo async => - _cachedIosDeviceInfo ??= await DeviceInfoPlatform.instance.iosInfo(); -} diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml deleted file mode 100644 index d557e496dcab..000000000000 --- a/packages/device_info/device_info/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: device_info -description: Flutter plugin providing detailed information about the device - (make, model, etc.), and Android or iOS version the app is running on. -repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 -version: 2.0.3 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.deviceinfo - pluginClass: DeviceInfoPlugin - ios: - pluginClass: FLTDeviceInfoPlugin - -dependencies: - flutter: - sdk: flutter - device_info_platform_interface: ^2.0.0 -dev_dependencies: - test: ^1.16.3 - flutter_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/AUTHORS b/packages/device_info/device_info_platform_interface/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/device_info/device_info_platform_interface/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/device_info/device_info_platform_interface/CHANGELOG.md b/packages/device_info/device_info_platform_interface/CHANGELOG.md deleted file mode 100644 index 438d5bccad40..000000000000 --- a/packages/device_info/device_info_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. -* Make `baseOS`, `previewSdkInt`, and `securityPatch` nullable types. -* Remove default values for non-nullable types. - -## 1.0.2 - -- Update Flutter SDK constraint. - -## 1.0.1 - -- Documentation typo fixed. - -## 1.0.0 - -- Initial open-source release. diff --git a/packages/device_info/device_info_platform_interface/LICENSE b/packages/device_info/device_info_platform_interface/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/device_info/device_info_platform_interface/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/device_info/device_info_platform_interface/README.md b/packages/device_info/device_info_platform_interface/README.md deleted file mode 100644 index 1391ffded5ee..000000000000 --- a/packages/device_info/device_info_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# device_info_platform_interface - -A common platform interface for the [`device_info`][1] plugin. - -This interface allows platform-specific implementations of the `device_info` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `device_info`, extend -[`DeviceInfoPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`DeviceInfoPlatform` by calling -`DeviceInfoPlatform.instance = MyPlatformDeviceInfo()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../device_info -[2]: lib/device_info_platform_interface.dart diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart deleted file mode 100644 index a40363b2dcb6..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'method_channel/method_channel_device_info.dart'; -import 'model/android_device_info.dart'; -import 'model/ios_device_info.dart'; -export 'model/android_device_info.dart'; -export 'model/ios_device_info.dart'; - -/// The interface that implementations of device_info must implement. -/// -/// Platform implementations should extend this class rather than implement it as `device_info` -/// does not consider newly added methods to be breaking changes. Extending this class -/// (using `extends`) ensures that the subclass will get the default implementation, while -/// platform implementations that `implements` this interface will be broken by newly added -/// [DeviceInfoPlatform] methods. -abstract class DeviceInfoPlatform extends PlatformInterface { - /// Constructs a DeviceInfoPlatform. - DeviceInfoPlatform() : super(token: _token); - - static final Object _token = Object(); - - static DeviceInfoPlatform _instance = MethodChannelDeviceInfo(); - - /// The default instance of [DeviceInfoPlatform] to use. - /// - /// Defaults to [MethodChannelDeviceInfo]. - static DeviceInfoPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [DeviceInfoPlatform] when they register themselves. - static set instance(DeviceInfoPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Gets the Android device information. - Future androidInfo() { - throw UnimplementedError('androidInfo() has not been implemented.'); - } - - /// Gets the iOS device information. - Future iosInfo() { - throw UnimplementedError('iosInfo() has not been implemented.'); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart deleted file mode 100644 index 3c19e57f66a8..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; -import 'package:device_info_platform_interface/device_info_platform_interface.dart'; - -/// An implementation of [DeviceInfoPlatform] that uses method channels. -class MethodChannelDeviceInfo extends DeviceInfoPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel channel = MethodChannel('plugins.flutter.io/device_info'); - - // Method channel for Android devices - Future androidInfo() async { - return AndroidDeviceInfo.fromMap((await channel - .invokeMapMethod('getAndroidDeviceInfo')) ?? - {}); - } - - // Method channel for iOS devices - Future iosInfo() async { - return IosDeviceInfo.fromMap( - (await channel.invokeMapMethod('getIosDeviceInfo')) ?? - {}); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart deleted file mode 100644 index b61dc14a0420..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart +++ /dev/null @@ -1,247 +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. - -/// Information derived from `android.os.Build`. -/// -/// See: https://developer.android.com/reference/android/os/Build.html -class AndroidDeviceInfo { - /// Android device Info class. - AndroidDeviceInfo({ - required this.version, - required this.board, - required this.bootloader, - required this.brand, - required this.device, - required this.display, - required this.fingerprint, - required this.hardware, - required this.host, - required this.id, - required this.manufacturer, - required this.model, - required this.product, - required List supported32BitAbis, - required List supported64BitAbis, - required List supportedAbis, - required this.tags, - required this.type, - required this.isPhysicalDevice, - required this.androidId, - required List systemFeatures, - }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), - supported64BitAbis = List.unmodifiable(supported64BitAbis), - supportedAbis = List.unmodifiable(supportedAbis), - systemFeatures = List.unmodifiable(systemFeatures); - - /// Android operating system version values derived from `android.os.Build.VERSION`. - final AndroidBuildVersion version; - - /// The name of the underlying board, like "goldfish". - /// - /// The value is an empty String if it is not available. - final String board; - - /// The system bootloader version number. - /// - /// The value is an empty String if it is not available. - final String bootloader; - - /// The consumer-visible brand with which the product/hardware will be associated, if any. - /// - /// The value is an empty String if it is not available. - final String brand; - - /// The name of the industrial design. - /// - /// The value is an empty String if it is not available. - final String device; - - /// A build ID string meant for displaying to the user. - /// - /// The value is an empty String if it is not available. - final String display; - - /// A string that uniquely identifies this build. - /// - /// The value is an empty String if it is not available. - final String fingerprint; - - /// The name of the hardware (from the kernel command line or /proc). - /// - /// The value is an empty String if it is not available. - final String hardware; - - /// Hostname. - /// - /// The value is an empty String if it is not available. - final String host; - - /// Either a changelist number, or a label like "M4-rc20". - /// - /// The value is an empty String if it is not available. - final String id; - - /// The manufacturer of the product/hardware. - /// - /// The value is an empty String if it is not available. - final String manufacturer; - - /// The end-user-visible name for the end product. - /// - /// The value is an empty String if it is not available. - final String model; - - /// The name of the overall product. - /// - /// The value is an empty String if it is not available. - final String product; - - /// An ordered list of 32 bit ABIs supported by this device. - final List supported32BitAbis; - - /// An ordered list of 64 bit ABIs supported by this device. - final List supported64BitAbis; - - /// An ordered list of ABIs supported by this device. - final List supportedAbis; - - /// Comma-separated tags describing the build, like "unsigned,debug". - /// - /// The value is an empty String if it is not available. - final String tags; - - /// The type of build, like "user" or "eng". - /// - /// The value is an empty String if it is not available. - final String type; - - /// The value is `true` if the application is running on a physical device. - /// - /// The value is `false` when the application is running on a emulator, or the value is unavailable. - final bool isPhysicalDevice; - - /// The Android hardware device ID that is unique between the device + user and app signing. - /// - /// The value is an empty String if it is not available. - final String androidId; - - /// Describes what features are available on the current device. - /// - /// This can be used to check if the device has, for example, a front-facing - /// camera, or a touchscreen. However, in many cases this is not the best - /// API to use. For example, if you are interested in bluetooth, this API - /// can tell you if the device has a bluetooth radio, but it cannot tell you - /// if bluetooth is currently enabled, or if you have been granted the - /// necessary permissions to use it. Please *only* use this if there is no - /// other way to determine if a feature is supported. - /// - /// This data comes from Android's PackageManager.getSystemAvailableFeatures, - /// and many of the common feature strings to look for are available in - /// PackageManager's public documentation: - /// https://developer.android.com/reference/android/content/pm/PackageManager - final List systemFeatures; - - /// Deserializes from the message received from [_kChannel]. - static AndroidDeviceInfo fromMap(Map map) { - return AndroidDeviceInfo( - version: AndroidBuildVersion._fromMap(map['version'] != null - ? map['version'].cast() - : {}), - board: map['board'] ?? '', - bootloader: map['bootloader'] ?? '', - brand: map['brand'] ?? '', - device: map['device'] ?? '', - display: map['display'] ?? '', - fingerprint: map['fingerprint'] ?? '', - hardware: map['hardware'] ?? '', - host: map['host'] ?? '', - id: map['id'] ?? '', - manufacturer: map['manufacturer'] ?? '', - model: map['model'] ?? '', - product: map['product'] ?? '', - supported32BitAbis: _fromList(map['supported32BitAbis']), - supported64BitAbis: _fromList(map['supported64BitAbis']), - supportedAbis: _fromList(map['supportedAbis']), - tags: map['tags'] ?? '', - type: map['type'] ?? '', - isPhysicalDevice: map['isPhysicalDevice'] ?? false, - androidId: map['androidId'] ?? '', - systemFeatures: _fromList(map['systemFeatures']), - ); - } - - /// Deserializes message as List - static List _fromList(dynamic message) { - if (message == null) { - return []; - } - assert(message is List); - final List list = List.from(message) - ..removeWhere((value) => value == null); - return list.cast(); - } -} - -/// Version values of the current Android operating system build derived from -/// `android.os.Build.VERSION`. -/// -/// See: https://developer.android.com/reference/android/os/Build.VERSION.html -class AndroidBuildVersion { - AndroidBuildVersion._({ - this.baseOS, - this.previewSdkInt, - this.securityPatch, - required this.codename, - required this.incremental, - required this.release, - required this.sdkInt, - }); - - /// The base OS build the product is based on. - /// This is only available on Android 6.0 or above. - String? baseOS; - - /// The developer preview revision of a prerelease SDK. - /// This is only available on Android 6.0 or above. - int? previewSdkInt; - - /// The user-visible security patch level. - /// This is only available on Android 6.0 or above. - final String? securityPatch; - - /// The current development codename, or the string "REL" if this is a release build. - /// - /// The value is an empty String if it is not available. - final String codename; - - /// The internal value used by the underlying source control to represent this build. - /// - /// The value is an empty String if it is not available. - final String incremental; - - /// The user-visible version string. - /// - /// The value is an empty String if it is not available. - final String release; - - /// The user-visible SDK version of the framework. - /// - /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html - /// - /// The value is -1 if it is unavailable. - final int sdkInt; - - /// Deserializes from the map message received from [_kChannel]. - static AndroidBuildVersion _fromMap(Map map) { - return AndroidBuildVersion._( - baseOS: map['baseOS'], - previewSdkInt: map['previewSdkInt'], - securityPatch: map['securityPatch'], - codename: map['codename'] ?? '', - incremental: map['incremental'] ?? '', - release: map['release'] ?? '', - sdkInt: map['sdkInt'] ?? -1, - ); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart deleted file mode 100644 index 17c96a3e250a..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart +++ /dev/null @@ -1,126 +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. - -/// Information derived from `UIDevice`. -/// -/// See: https://developer.apple.com/documentation/uikit/uidevice -class IosDeviceInfo { - /// IOS device info class. - IosDeviceInfo({ - required this.name, - required this.systemName, - required this.systemVersion, - required this.model, - required this.localizedModel, - required this.identifierForVendor, - required this.isPhysicalDevice, - required this.utsname, - }); - - /// Device name. - /// - /// The value is an empty String if it is not available. - final String name; - - /// The name of the current operating system. - /// - /// The value is an empty String if it is not available. - final String systemName; - - /// The current operating system version. - /// - /// The value is an empty String if it is not available. - final String systemVersion; - - /// Device model. - /// - /// The value is an empty String if it is not available. - final String model; - - /// Localized name of the device model. - /// - /// The value is an empty String if it is not available. - final String localizedModel; - - /// Unique UUID value identifying the current device. - /// - /// The value is an empty String if it is not available. - final String identifierForVendor; - - /// The value is `true` if the application is running on a physical device. - /// - /// The value is `false` when the application is running on a simulator, or the value is unavailable. - final bool isPhysicalDevice; - - /// Operating system information derived from `sys/utsname.h`. - /// - /// The value is an empty String if it is not available. - final IosUtsname utsname; - - /// Deserializes from the map message received from [_kChannel]. - static IosDeviceInfo fromMap(Map map) { - return IosDeviceInfo( - name: map['name'] ?? '', - systemName: map['systemName'] ?? '', - systemVersion: map['systemVersion'] ?? '', - model: map['model'] ?? '', - localizedModel: map['localizedModel'] ?? '', - identifierForVendor: map['identifierForVendor'] ?? '', - isPhysicalDevice: map['isPhysicalDevice'] != null - ? map['isPhysicalDevice'] == 'true' - : false, - utsname: IosUtsname._fromMap(map['utsname'] != null - ? map['utsname'].cast() - : {}), - ); - } -} - -/// Information derived from `utsname`. -/// See http://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html for details. -class IosUtsname { - IosUtsname._({ - required this.sysname, - required this.nodename, - required this.release, - required this.version, - required this.machine, - }); - - /// Operating system name. - /// - /// The value is an empty String if it is not available. - final String sysname; - - /// Network node name. - /// - /// The value is an empty String if it is not available. - final String nodename; - - /// Release level. - /// - /// The value is an empty String if it is not available. - final String release; - - /// Version level. - /// - /// The value is an empty String if it is not available. - final String version; - - /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). - /// - /// The value is an empty String if it is not available. - final String machine; - - /// Deserializes from the map message received from [_kChannel]. - static IosUtsname _fromMap(Map map) { - return IosUtsname._( - sysname: map['sysname'] ?? '', - nodename: map['nodename'] ?? '', - release: map['release'] ?? '', - version: map['version'] ?? '', - machine: map['machine'] ?? '', - ); - } -} diff --git a/packages/device_info/device_info_platform_interface/pubspec.yaml b/packages/device_info/device_info_platform_interface/pubspec.yaml deleted file mode 100644 index cf3e50f98422..000000000000 --- a/packages/device_info/device_info_platform_interface/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: device_info_platform_interface -description: A common platform interface for the device_info plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 -# NOTE: We strongly prefer non-breaking changes, even at the expense of a -# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4" - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - test: ^1.16.3 - pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart deleted file mode 100644 index 14ed7c0aefb4..000000000000 --- a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:device_info_platform_interface/device_info_platform_interface.dart'; -import 'package:device_info_platform_interface/method_channel/method_channel_device_info.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group("$MethodChannelDeviceInfo", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "version": { - "securityPatch": "2018-09-05", - "sdkInt": 28, - "release": "9", - "previewSdkInt": 0, - "incremental": "5124027", - "codename": "REL", - "baseOS": "", - }, - "board": "goldfish_x86_64", - "bootloader": "unknown", - "brand": "google", - "device": "generic_x86_64", - "display": "PSR1.180720.075", - "fingerprint": - "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys", - "hardware": "ranchu", - "host": "abfarm730", - "id": "PSR1.180720.075", - "manufacturer": "Google", - "model": "Android SDK built for x86_64", - "product": "sdk_gphone_x86_64", - "supported32BitAbis": [ - "x86", - ], - "supported64BitAbis": [ - "x86_64", - ], - "supportedAbis": [ - "x86_64", - "x86", - ], - "tags": "release-keys", - "type": "user", - "isPhysicalDevice": false, - "androidId": "f47571f3b4648f45", - "systemFeatures": [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ], - }); - case 'getIosDeviceInfo': - return ({ - "name": "iPhone 13", - "systemName": "iOS", - "systemVersion": "13.0", - "model": "iPhone", - "localizedModel": "iPhone", - "identifierForVendor": "88F59280-55AD-402C-B922-3203B4794C06", - "isPhysicalDevice": false, - "utsname": { - "sysname": "Darwin", - "nodename": "host", - "release": "19.6.0", - "version": - "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64", - "machine": "x86_64", - } - }); - default: - return null; - } - }); - }); - - test("androidInfo", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, "2018-09-05"); - expect(result.version.sdkInt, 28); - expect(result.version.release, "9"); - expect(result.version.previewSdkInt, 0); - expect(result.version.incremental, "5124027"); - expect(result.version.codename, "REL"); - expect(result.board, "goldfish_x86_64"); - expect(result.bootloader, "unknown"); - expect(result.brand, "google"); - expect(result.device, "generic_x86_64"); - expect(result.display, "PSR1.180720.075"); - expect(result.fingerprint, - "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys"); - expect(result.hardware, "ranchu"); - expect(result.host, "abfarm730"); - expect(result.id, "PSR1.180720.075"); - expect(result.manufacturer, "Google"); - expect(result.model, "Android SDK built for x86_64"); - expect(result.product, "sdk_gphone_x86_64"); - expect(result.supported32BitAbis, [ - "x86", - ]); - expect(result.supported64BitAbis, [ - "x86_64", - ]); - expect(result.supportedAbis, [ - "x86_64", - "x86", - ]); - expect(result.tags, "release-keys"); - expect(result.type, "user"); - expect(result.isPhysicalDevice, false); - expect(result.androidId, "f47571f3b4648f45"); - expect(result.systemFeatures, [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ]); - }); - - test("iosInfo", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, "iPhone 13"); - expect(result.systemName, "iOS"); - expect(result.systemVersion, "13.0"); - expect(result.model, "iPhone"); - expect(result.localizedModel, "iPhone"); - expect( - result.identifierForVendor, "88F59280-55AD-402C-B922-3203B4794C06"); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, "Darwin"); - expect(result.utsname.nodename, "host"); - expect(result.utsname.release, "19.6.0"); - expect(result.utsname.version, - "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64"); - expect(result.utsname.machine, "x86_64"); - }); - }); - - group( - "$MethodChannelDeviceInfo handles null value in the map returned from method channel", - () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "version": null, - "board": null, - "bootloader": null, - "brand": null, - "device": null, - "display": null, - "fingerprint": null, - "hardware": null, - "host": null, - "id": null, - "manufacturer": null, - "model": null, - "product": null, - "supported32BitAbis": null, - "supported64BitAbis": null, - "supportedAbis": null, - "tags": null, - "type": null, - "isPhysicalDevice": null, - "androidId": null, - "systemFeatures": null, - }); - case 'getIosDeviceInfo': - return ({ - "name": null, - "systemName": null, - "systemVersion": null, - "model": null, - "localizedModel": null, - "identifierForVendor": null, - "isPhysicalDevice": null, - "utsname": null, - }); - default: - return null; - } - }); - }); - - test("androidInfo hanels null", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, null); - expect(result.version.sdkInt, -1); - expect(result.version.release, ''); - expect(result.version.previewSdkInt, null); - expect(result.version.incremental, ''); - expect(result.version.codename, ''); - expect(result.board, ''); - expect(result.bootloader, ''); - expect(result.brand, ''); - expect(result.device, ''); - expect(result.display, ''); - expect(result.fingerprint, ''); - expect(result.hardware, ''); - expect(result.host, ''); - expect(result.id, ''); - expect(result.manufacturer, ''); - expect(result.model, ''); - expect(result.product, ''); - expect(result.supported32BitAbis, []); - expect(result.supported64BitAbis, []); - expect(result.supportedAbis, []); - expect(result.tags, ''); - expect(result.type, ''); - expect(result.isPhysicalDevice, false); - expect(result.androidId, ''); - expect(result.systemFeatures, []); - }); - - test("iosInfo handles null", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, ''); - expect(result.systemName, ''); - expect(result.systemVersion, ''); - expect(result.model, ''); - expect(result.localizedModel, ''); - expect(result.identifierForVendor, ''); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, ''); - expect(result.utsname.nodename, ''); - expect(result.utsname.release, ''); - expect(result.utsname.version, ''); - expect(result.utsname.machine, ''); - }); - }); - - group("$MethodChannelDeviceInfo handles method channel returns null", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return null; - case 'getIosDeviceInfo': - return null; - default: - return null; - } - }); - }); - - test("androidInfo handles null", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, null); - expect(result.version.sdkInt, -1); - expect(result.version.release, ''); - expect(result.version.previewSdkInt, null); - expect(result.version.incremental, ''); - expect(result.version.codename, ''); - expect(result.board, ''); - expect(result.bootloader, ''); - expect(result.brand, ''); - expect(result.device, ''); - expect(result.display, ''); - expect(result.fingerprint, ''); - expect(result.hardware, ''); - expect(result.host, ''); - expect(result.id, ''); - expect(result.manufacturer, ''); - expect(result.model, ''); - expect(result.product, ''); - expect(result.supported32BitAbis, []); - expect(result.supported64BitAbis, []); - expect(result.supportedAbis, []); - expect(result.tags, ''); - expect(result.type, ''); - expect(result.isPhysicalDevice, false); - expect(result.androidId, ''); - expect(result.systemFeatures, []); - }); - - test("iosInfo handles null", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, ''); - expect(result.systemName, ''); - expect(result.systemVersion, ''); - expect(result.model, ''); - expect(result.localizedModel, ''); - expect(result.identifierForVendor, ''); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, ''); - expect(result.utsname.nodename, ''); - expect(result.utsname.release, ''); - expect(result.utsname.version, ''); - expect(result.utsname.machine, ''); - }); - }); - - group("$MethodChannelDeviceInfo android handles null values in list", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "supported32BitAbis": ["x86"], - "supported64BitAbis": ["x86_64"], - "supportedAbis": ["x86_64", "x86"], - "systemFeatures": [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ], - }); - default: - return null; - } - }); - }); - - test("androidInfo hanels null in list", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - expect(result.supported32BitAbis, ['x86']); - expect(result.supported64BitAbis, ['x86_64']); - expect(result.supportedAbis, ['x86_64', 'x86']); - expect(result.systemFeatures, [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen" - ]); - }); - }); -} diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index 8dadde267e8a..306a857731e8 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: connectivity_for_web_integration_tests +name: image_picker_for_web_integration_tests publish_to: none environment: diff --git a/packages/package_info/AUTHORS b/packages/package_info/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/package_info/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md deleted file mode 100644 index ca1a943d644a..000000000000 --- a/packages/package_info/CHANGELOG.md +++ /dev/null @@ -1,188 +0,0 @@ -## NEXT - -* Remove references to the Android v1 embedding. -* Updated Android lint settings. -* Updates Android compileSdkVersion to 31. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 0.4.3+4 - -* Ensure `IntegrationTestPlugin` is registered in `example` app, so Firebase Test Lab tests report test results correctly. [Issue](https://github.com/flutter/flutter/issues/74944). - -## 0.4.3+3 - -* Update Flutter SDK constraint. - -## 0.4.3+2 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.4.3+1 - -* Update Android compileSdkVersion to 29. - -## 0.4.3 - -* Update package:e2e -> package:integration_test - -## 0.4.2 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.1 - -* Add support for macOS. - -## 0.4.0+18 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.0+17 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. -* Fix CocoaPods podspec lint warnings. - -## 0.4.0+16 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.0+15 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.0+14 - -* Make the pedantic dev_dependency explicit. - -## 0.4.0+13 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.0+12 - -* Fix pedantic lints. This involved internally refactoring how the - `PackageInfo.fromPlatform` code handled futures, but shouldn't change existing - functionality. - -## 0.4.0+11 - -* Remove AndroidX warnings. - -## 0.4.0+10 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.0+9 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.4.0+8 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Add a unit test. -* Migrate to using the new e2e test binding. - -## 0.4.0+7 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.0+6 - -* Fix Android compiler warnings. - -## 0.4.0+5 - -* Add iOS-specific warning to README.md. - -## 0.4.0+4 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.4.0+3 - -* Add integration test. - -## 0.4.0+2 - -* Android: Using new method for BuildNumber in new android versions - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.2+1 - -* Fixed a crash on IOS when some of the package infos are not available. - -## 0.3.2 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.3.1 - -* Added `appName` field to `PackageInfo` for getting the display name of the app. - -## 0.3.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.2.1 - -* Fixed Dart 2 type error. - -## 0.2.0 - -* **Breaking change**. Introduced class `PackageInfo` in place of individual functions. -* `PackageInfo` provides all package information with a single async call. - -## 0.1.1 - -* Added package name to available information. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.2 - -* Add FLT prefix to iOS types - -## 0.0.1 - -* Initial release diff --git a/packages/package_info/LICENSE b/packages/package_info/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/package_info/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/package_info/README.md b/packages/package_info/README.md deleted file mode 100644 index 80893880f3c2..000000000000 --- a/packages/package_info/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# PackageInfo - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`package_info_plus`](https://pub.dev/packages/package_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This Flutter plugin provides an API for querying information about an -application package. - -# Usage - -You can use the PackageInfo to query information about the -application package. This works both on iOS and Android. - -```dart -import 'package:package_info/package_info.dart'; - -PackageInfo packageInfo = await PackageInfo.fromPlatform(); - -String appName = packageInfo.appName; -String packageName = packageInfo.packageName; -String version = packageInfo.version; -String buildNumber = packageInfo.buildNumber; -``` - -Or in async mode: - -```dart -PackageInfo.fromPlatform().then((PackageInfo packageInfo) { - String appName = packageInfo.appName; - String packageName = packageInfo.packageName; - String version = packageInfo.version; - String buildNumber = packageInfo.buildNumber; -}); -``` - -## Known Issue - -As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS -requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. -Clean the Xcode build folder with: -`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. - -## Issues and feedback - -Please file [issues](https://github.com/flutter/flutter/issues/new) to send feedback or report a bug. Thank you! diff --git a/packages/package_info/analysis_options.yaml b/packages/package_info/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/package_info/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle deleted file mode 100644 index ad620add0f93..000000000000 --- a/packages/package_info/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.packageinfo' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 31 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/package_info/android/settings.gradle b/packages/package_info/android/settings.gradle deleted file mode 100644 index a5683f94fce7..000000000000 --- a/packages/package_info/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'package_info' diff --git a/packages/package_info/android/src/main/AndroidManifest.xml b/packages/package_info/android/src/main/AndroidManifest.xml deleted file mode 100644 index 133ae5faf3c7..000000000000 --- a/packages/package_info/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java b/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java deleted file mode 100644 index 4611f70951f9..000000000000 --- a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java +++ /dev/null @@ -1,79 +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.packageinfo; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import java.util.HashMap; -import java.util.Map; - -/** PackageInfoPlugin */ -public class PackageInfoPlugin implements MethodCallHandler, FlutterPlugin { - private Context applicationContext; - private MethodChannel methodChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - final PackageInfoPlugin instance = new PackageInfoPlugin(); - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - this.applicationContext = applicationContext; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/package_info"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - applicationContext = null; - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - try { - if (call.method.equals("getAll")) { - PackageManager pm = applicationContext.getPackageManager(); - PackageInfo info = pm.getPackageInfo(applicationContext.getPackageName(), 0); - - Map map = new HashMap<>(); - map.put("appName", info.applicationInfo.loadLabel(pm).toString()); - map.put("packageName", applicationContext.getPackageName()); - map.put("version", info.versionName); - map.put("buildNumber", String.valueOf(getLongVersionCode(info))); - - result.success(map); - } else { - result.notImplemented(); - } - } catch (PackageManager.NameNotFoundException ex) { - result.error("Name not found", ex.getMessage(), null); - } - } - - @SuppressWarnings("deprecation") - private static long getLongVersionCode(PackageInfo info) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return info.getLongVersionCode(); - } - return info.versionCode; - } -} diff --git a/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m b/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m deleted file mode 100644 index ab686fa08676..000000000000 --- a/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTPackageInfoPlugin.h" - -@implementation FLTPackageInfoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/package_info" - binaryMessenger:[registrar messenger]]; - FLTPackageInfoPlugin* instance = [[FLTPackageInfoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"getAll"]) { - result(@{ - @"appName" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] - ?: [NSNull null], - @"packageName" : [[NSBundle mainBundle] bundleIdentifier] ?: [NSNull null], - @"version" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] - ?: [NSNull null], - @"buildNumber" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] - ?: [NSNull null], - }); - } else { - result(FlutterMethodNotImplemented); - } -} - -@end diff --git a/packages/package_info/example/README.md b/packages/package_info/example/README.md deleted file mode 100644 index 762d04ec0532..000000000000 --- a/packages/package_info/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# package_info_example - -Demonstrates how to use the package_info plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/package_info/example/android.iml b/packages/package_info/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/example/android/app/build.gradle b/packages/package_info/example/android/app/build.gradle deleted file mode 100644 index 60fc74107b0f..000000000000 --- a/packages/package_info/example/android/app/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 31 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.packageinfoexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/package_info/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/package_info/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/package_info/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java deleted file mode 100644 index 0f4298dca155..000000000000 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java +++ /dev/null @@ -1,14 +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; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface DartIntegrationTest {} diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java deleted file mode 100644 index fb63f6f8c88b..000000000000 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java +++ /dev/null @@ -1,19 +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.packageinfoexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.plugins.DartIntegrationTest; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@DartIntegrationTest -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/package_info/example/android/app/src/main/AndroidManifest.xml b/packages/package_info/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index efb42ac02c5c..000000000000 --- a/packages/package_info/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/package_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/package_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/package_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/package_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/package_info/example/android/build.gradle b/packages/package_info/example/android/build.gradle deleted file mode 100644 index 64450a26d537..000000000000 --- a/packages/package_info/example/android/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -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}" - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/package_info/example/android/gradle.properties b/packages/package_info/example/android/gradle.properties deleted file mode 100644 index 05413bc45d00..000000000000 --- a/packages/package_info/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableJetifier=true -android.enableR8=true -android.useAndroidX=true diff --git a/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/package_info/example/android/settings.gradle b/packages/package_info/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/package_info/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/packages/package_info/example/integration_test/package_info_test.dart b/packages/package_info/example/integration_test/package_info_test.dart deleted file mode 100644 index ab8f5f38b472..000000000000 --- a/packages/package_info/example/integration_test/package_info_test.dart +++ /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. - -// @dart=2.9 - -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:package_info/package_info.dart'; -import 'package:package_info_example/main.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('fromPlatform', (WidgetTester tester) async { - final PackageInfo info = await PackageInfo.fromPlatform(); - // These tests are based on the example app. The tests should be updated if any related info changes. - if (Platform.isAndroid) { - expect(info.appName, 'package_info_example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); - expect(info.version, '1.0'); - } else if (Platform.isIOS) { - expect(info.appName, 'Package Info Example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); - expect(info.version, '1.0'); - } else if (Platform.isMacOS) { - expect(info.appName, 'Package Info Example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); - expect(info.version, '1.0.0'); - } else { - throw (UnsupportedError('platform not supported')); - } - }); - - testWidgets('example', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); - await tester.pumpAndSettle(); - if (Platform.isAndroid) { - expect(find.text('package_info_example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('io.flutter.plugins.packageinfoexample'), findsOneWidget); - expect(find.text('1.0'), findsOneWidget); - } else if (Platform.isIOS) { - expect(find.text('Package Info Example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); - expect(find.text('1.0'), findsOneWidget); - } else if (Platform.isMacOS) { - expect(find.text('Package Info Example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); - expect(find.text('1.0.0'), findsOneWidget); - } else { - throw (UnsupportedError('platform not supported')); - } - }); -} diff --git a/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist b/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/package_info/example/ios/Flutter/Debug.xcconfig b/packages/package_info/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/package_info/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/package_info/example/ios/Flutter/Release.xcconfig b/packages/package_info/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/package_info/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/package_info/example/ios/Podfile b/packages/package_info/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/package_info/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f21209190faa..000000000000 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 */; }; - C824856099B606661DF36830 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D007BB586407934FC28AF83 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 0B4CD678E84FD9C2C80D895C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 = ""; }; - 7D007BB586407934FC28AF83 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 8F88DBCB0DD2793F05ADE394 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C824856099B606661DF36830 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1B8D0C5C4E228D9E0271D922 /* Pods */ = { - isa = PBXGroup; - children = ( - 0B4CD678E84FD9C2C80D895C /* Pods-Runner.debug.xcconfig */, - 8F88DBCB0DD2793F05ADE394 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 1B8D0C5C4E228D9E0271D922 /* Pods */, - F2F265795CE7F5960E889E92 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - F2F265795CE7F5960E889E92 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7D007BB586407934FC28AF83 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AA8267A332E7FFF199F5E510 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AA8267A332E7FFF199F5E510 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample; - 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.packageInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6254f..000000000000 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/package_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/package_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/package_info/example/ios/Runner/AppDelegate.h b/packages/package_info/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 0681d288bb70..000000000000 --- a/packages/package_info/example/ios/Runner/AppDelegate.h +++ /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. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/package_info/example/ios/Runner/AppDelegate.m b/packages/package_info/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 30b87969f44a..000000000000 --- a/packages/package_info/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +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. - -#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/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/ios/Runner/Base.lproj/Main.storyboard b/packages/package_info/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/package_info/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/ios/Runner/Info.plist b/packages/package_info/example/ios/Runner/Info.plist deleted file mode 100644 index 0ba5e95cf616..000000000000 --- a/packages/package_info/example/ios/Runner/Info.plist +++ /dev/null @@ -1,51 +0,0 @@ - - - - - CFBundleDisplayName - Package Info Example - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - package_info_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/package_info/example/ios/Runner/main.m b/packages/package_info/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/package_info/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/package_info/example/lib/main.dart b/packages/package_info/example/lib/main.dart deleted file mode 100644 index 60e4a16f7817..000000000000 --- a/packages/package_info/example/lib/main.dart +++ /dev/null @@ -1,81 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:package_info/package_info.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'PackageInfo Demo', - theme: ThemeData(primarySwatch: Colors.blue), - home: MyHomePage(title: 'PackageInfo example app'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - PackageInfo _packageInfo = PackageInfo( - appName: 'Unknown', - packageName: 'Unknown', - version: 'Unknown', - buildNumber: 'Unknown', - ); - - @override - void initState() { - super.initState(); - _initPackageInfo(); - } - - Future _initPackageInfo() async { - final PackageInfo info = await PackageInfo.fromPlatform(); - setState(() { - _packageInfo = info; - }); - } - - Widget _infoTile(String title, String subtitle) { - return ListTile( - title: Text(title), - subtitle: Text(subtitle.isNotEmpty ? subtitle : 'Not set'), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _infoTile('App name', _packageInfo.appName), - _infoTile('Package name', _packageInfo.packageName), - _infoTile('App version', _packageInfo.version), - _infoTile('Build number', _packageInfo.buildNumber), - ], - ), - ); - } -} diff --git a/packages/package_info/example/macos/.gitignore b/packages/package_info/example/macos/.gitignore deleted file mode 100644 index d2fd3772308c..000000000000 --- a/packages/package_info/example/macos/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/xcuserdata/ diff --git a/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig b/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/package_info/example/macos/Podfile b/packages/package_info/example/macos/Podfile deleted file mode 100644 index dade8dfad0dc..000000000000 --- a/packages/package_info/example/macos/Podfile +++ /dev/null @@ -1,40 +0,0 @@ -platform :osx, '10.11' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 3525d85d6678..000000000000 --- a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,644 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 26D1A257E90EADFCCC251DEE /* 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 = ""; }; - 2DB1F806EBAC0EF2659B294F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* Package Info Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Package Info Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 58201D283908D33A078698CD /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* Package Info Example.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - 58201D283908D33A078698CD /* Pods */ = { - isa = PBXGroup; - children = ( - 26D1A257E90EADFCCC251DEE /* Pods-Runner.debug.xcconfig */, - B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */, - 2DB1F806EBAC0EF2659B294F /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - A7B87C53ACB98DD8DB15DE02 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - F39B8515B2FA626EA800A9B8 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Package Info Example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; - }; - A7B87C53ACB98DD8DB15DE02 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - F39B8515B2FA626EA800A9B8 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/package_info/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/package_info/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 9c27c87c30ba..000000000000 --- a/packages/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/package_info/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/package_info/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/package_info/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/package_info/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/package_info/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/package_info/example/macos/Runner/AppDelegate.swift b/packages/package_info/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index 5cec4c48f620..000000000000 --- a/packages/package_info/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f11..000000000000 --- a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46993 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 diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3276 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 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136a47ed24125c7efc79d68a4a01961b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14800 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 4ecd23f86c29..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = Package Info Example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2020 The Flutter Authors. All rights reserved. diff --git a/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig b/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464f4..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/package_info/example/macos/Runner/Configs/Release.xcconfig b/packages/package_info/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561c8..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig b/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780b1..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/package_info/example/macos/Runner/DebugProfile.entitlements b/packages/package_info/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/package_info/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/packages/package_info/example/macos/Runner/Info.plist b/packages/package_info/example/macos/Runner/Info.plist deleted file mode 100644 index a8e4f02255ba..000000000000 --- a/packages/package_info/example/macos/Runner/Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - CFBundleDisplayName - $(PRODUCT_NAME) - - diff --git a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift b/packages/package_info/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 32aaeedceb1f..000000000000 --- a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/packages/package_info/example/macos/Runner/Release.entitlements b/packages/package_info/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/package_info/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/package_info/example/package_info_example.iml b/packages/package_info/example/package_info_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/package_info/example/package_info_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/package_info/example/package_info_example_android.iml b/packages/package_info/example/package_info_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/example/package_info_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/example/pubspec.yaml b/packages/package_info/example/pubspec.yaml deleted file mode 100644 index 9c1c3cd0ad6e..000000000000 --- a/packages/package_info/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: package_info_example -description: Demonstrates how to use the package_info plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - package_info: - # When depending on this package from a real application you should use: - # package_info: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - integration_test: - sdk: flutter - -dev_dependencies: - flutter_driver: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/package_info/example/test_driver/integration_test.dart b/packages/package_info/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/package_info/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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. - -// @dart=2.9 - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/package_info/ios/Assets/.gitkeep b/packages/package_info/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h deleted file mode 100644 index 65be5f99d569..000000000000 --- a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTPackageInfoPlugin : NSObject -@end diff --git a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m deleted file mode 120000 index a1ddabd76793..000000000000 --- a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1 +0,0 @@ -../../darwin/Classes/FLTPackageInfoPlugin.m \ No newline at end of file diff --git a/packages/package_info/ios/package_info.podspec b/packages/package_info/ios/package_info.podspec deleted file mode 100644 index 12ccab3b855b..000000000000 --- a/packages/package_info/ios/package_info.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'package_info' - s.version = '0.0.1' - s.summary = 'Flutter Package Info' - s.description = <<-DESC -This Flutter plugin provides an API for querying information about an application package. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/package_info' } - s.documentation_url = 'https://pub.dev/packages/package_info' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/package_info/lib/package_info.dart b/packages/package_info/lib/package_info.dart deleted file mode 100644 index 69246813873a..000000000000 --- a/packages/package_info/lib/package_info.dart +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/package_info'); - -/// Application metadata. Provides application bundle information on iOS and -/// application package information on Android. -/// -/// ```dart -/// PackageInfo packageInfo = await PackageInfo.fromPlatform() -/// print("Version is: ${packageInfo.version}"); -/// ``` -class PackageInfo { - /// Constructs an instance with the given values for testing. [PackageInfo] - /// instances constructed this way won't actually reflect any real information - /// from the platform, just whatever was passed in at construction time. - /// - /// See [fromPlatform] for the right API to get a [PackageInfo] that's - /// actually populated with real data. - PackageInfo({ - required this.appName, - required this.packageName, - required this.version, - required this.buildNumber, - }); - - static PackageInfo? _fromPlatform; - - /// Retrieves package information from the platform. - /// The result is cached. - static Future fromPlatform() async { - PackageInfo? packageInfo = _fromPlatform; - if (packageInfo != null) return packageInfo; - - final Map map = - (await _kChannel.invokeMapMethod('getAll'))!; - - packageInfo = PackageInfo( - appName: map["appName"] ?? '', - packageName: map["packageName"] ?? '', - version: map["version"] ?? '', - buildNumber: map["buildNumber"] ?? '', - ); - _fromPlatform = packageInfo; - return packageInfo; - } - - /// The app name. `CFBundleDisplayName` on iOS, `application/label` on Android. - final String appName; - - /// The package name. `bundleIdentifier` on iOS, `getPackageName` on Android. - final String packageName; - - /// The package version. `CFBundleShortVersionString` on iOS, `versionName` on Android. - final String version; - - /// The build number. `CFBundleVersion` on iOS, `versionCode` on Android. - final String buildNumber; -} diff --git a/packages/package_info/macos/Assets/.gitkeep b/packages/package_info/macos/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h deleted file mode 100644 index 590e8263d951..000000000000 --- a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTPackageInfoPlugin : NSObject -@end diff --git a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m deleted file mode 120000 index a1ddabd76793..000000000000 --- a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1 +0,0 @@ -../../darwin/Classes/FLTPackageInfoPlugin.m \ No newline at end of file diff --git a/packages/package_info/macos/package_info.podspec b/packages/package_info/macos/package_info.podspec deleted file mode 100644 index dbe5bd9a105b..000000000000 --- a/packages/package_info/macos/package_info.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'package_info' - s.version = '0.0.1' - s.summary = 'Flutter plugin for querying information about the application package.' - s.description = <<-DESC -Flutter plugin for querying information about the application package, based on bundle data. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/package_info' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/package_info' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/package_info/package_info_android.iml b/packages/package_info/package_info_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/package_info_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml deleted file mode 100644 index dd9de6f14808..000000000000 --- a/packages/package_info/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: package_info -description: Flutter plugin for querying information about the application - package, such as CFBundleVersion on iOS or versionCode on Android. -repository: https://github.com/flutter/plugins/tree/master/packages/package_info -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+package_info%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.packageinfo - pluginClass: PackageInfoPlugin - ios: - pluginClass: FLTPackageInfoPlugin - macos: - pluginClass: FLTPackageInfoPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/package_info/test/package_info_test.dart b/packages/package_info/test/package_info_test.dart deleted file mode 100644 index 91661de72103..000000000000 --- a/packages/package_info/test/package_info_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:package_info/package_info.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const MethodChannel channel = - MethodChannel('plugins.flutter.io/package_info'); - late List log; - - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getAll': - return { - 'appName': 'package_info_example', - 'buildNumber': '1', - 'packageName': 'io.flutter.plugins.packageinfoexample', - 'version': '1.0', - }; - default: - assert(false); - return null; - } - }); - - setUp(() { - log = []; - }); - - test('fromPlatform', () async { - final PackageInfo info = await PackageInfo.fromPlatform(); - expect(info.appName, 'package_info_example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); - expect(info.version, '1.0'); - expect( - log, - [ - isMethodCall('getAll', arguments: null), - ], - ); - }); -} diff --git a/packages/sensors/AUTHORS b/packages/sensors/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/sensors/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md deleted file mode 100644 index b5d64b48edda..000000000000 --- a/packages/sensors/CHANGELOG.md +++ /dev/null @@ -1,178 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. -* Updates Android compileSdkVersion to 31. - -## 2.0.3 - -* Update README to point to Plus Plugins version. - -## 2.0.2 - -* Fix -Wstrict-prototypes analyzer warning in iOS plugin. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 0.4.2+8 - -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 0.4.2+7 - -* Update Flutter SDK constraint. - -## 0.4.2+6 - -* Update android compileSdkVersion to 29. - -## 0.4.2+5 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.2+4 - -* Update package:e2e -> package:integration_test - -## 0.4.2+3 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.2+2 - -* Post-v2 Android embedding cleanup. - -## 0.4.2+1 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.2 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. -* Fix CocoaPods podspec lint warnings. - -## 0.4.1+10 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.1+9 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.1+8 - -* Make the pedantic dev_dependency explicit. - -## 0.4.1+7 - -* Fixed example userAccelerometerEvent in documentation - -## 0.4.1+6 - -* Migrate from deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger. -* Require Flutter SDK 1.12.13+hotfix.5 or greater (current stable). - -## 0.4.1+5 - -* Fix example `setState()` called after `dispose()` by canceling the timer. - -## 0.4.1+4 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.1+3 - -* Improve documentation and add unit test coverage. - -## 0.4.1+2 - -* Remove AndroidX warnings. - -## 0.4.1+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.1 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - -## 0.4.0+3 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.0+2 - -* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446 - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.5 - -* Added missing test package dependency. - -## 0.3.4 - -* Make sensors Dart 2 compliant. - -## 0.3.3 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.3.2 - -* Added user acceleration sensor events (i.e. accelerometer without gravity). - -## 0.3.1 - -* Fixed Dart 2 type error with iOS sensor events. - -## 0.3.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.2.1 - -* Fixed warnings from the Dart 2.0 analyzer. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.2.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.1.1 - -* Added FLT prefix to iOS types. - -## 0.1.0 - -* Initial Open Source release. diff --git a/packages/sensors/LICENSE b/packages/sensors/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/sensors/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/sensors/README.md b/packages/sensors/README.md deleted file mode 100644 index 1f46ce1c3608..000000000000 --- a/packages/sensors/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# sensors - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`sensors_plus`](https://pub.dev/packages/sensors_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -A Flutter plugin to access the accelerometer and gyroscope sensors. - -## Usage - -To use this plugin, add `sensors` as a [dependency in your pubspec.yaml -file](https://flutter.dev/docs/development/platform-integration/platform-channels). - -This will expose three classes of sensor events, through three different -streams. - -- `AccelerometerEvent`s describe the velocity of the device, including the - effects of gravity. Put simply, you can use accelerometer readings to tell if - the device is moving in a particular direction. -- `UserAccelerometerEvent`s also describe the velocity of the device, but don't - include gravity. They can also be thought of as just the user's affect on the - device. -- `GyroscopeEvent`s describe the rotation of the device. - -Each of these is exposed through a `BroadcastStream`: `accelerometerEvents`, -`userAccelerometerEvents`, and `gyroscopeEvents`, respectively. - - -### Example - -``` dart -import 'package:sensors/sensors.dart'; - -accelerometerEvents.listen((AccelerometerEvent event) { - print(event); -}); -// [AccelerometerEvent (x: 0.0, y: 9.8, z: 0.0)] - -userAccelerometerEvents.listen((UserAccelerometerEvent event) { - print(event); -}); -// [UserAccelerometerEvent (x: 0.0, y: 0.0, z: 0.0)] - -gyroscopeEvents.listen((GyroscopeEvent event) { - print(event); -}); -// [GyroscopeEvent (x: 0.0, y: 0.0, z: 0.0)] - -``` - -Also see the `example` subdirectory for an example application that uses the -sensor data. diff --git a/packages/sensors/analysis_options.yaml b/packages/sensors/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/sensors/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle deleted file mode 100644 index a9a9045301c9..000000000000 --- a/packages/sensors/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.sensors' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 31 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/sensors/android/settings.gradle b/packages/sensors/android/settings.gradle deleted file mode 100644 index 48202890db16..000000000000 --- a/packages/sensors/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'sensors' diff --git a/packages/sensors/android/src/main/AndroidManifest.xml b/packages/sensors/android/src/main/AndroidManifest.xml deleted file mode 100644 index 44d0c9993ce9..000000000000 --- a/packages/sensors/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java b/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java deleted file mode 100644 index c643edce3401..000000000000 --- a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java +++ /dev/null @@ -1,72 +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.sensors; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorManager; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; - -/** SensorsPlugin */ -public class SensorsPlugin implements FlutterPlugin { - private static final String ACCELEROMETER_CHANNEL_NAME = - "plugins.flutter.io/sensors/accelerometer"; - private static final String GYROSCOPE_CHANNEL_NAME = "plugins.flutter.io/sensors/gyroscope"; - private static final String USER_ACCELEROMETER_CHANNEL_NAME = - "plugins.flutter.io/sensors/user_accel"; - - private EventChannel accelerometerChannel; - private EventChannel userAccelChannel; - private EventChannel gyroscopeChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - SensorsPlugin plugin = new SensorsPlugin(); - plugin.setupEventChannels(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - final Context context = binding.getApplicationContext(); - setupEventChannels(context, binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - teardownEventChannels(); - } - - private void setupEventChannels(Context context, BinaryMessenger messenger) { - accelerometerChannel = new EventChannel(messenger, ACCELEROMETER_CHANNEL_NAME); - final StreamHandlerImpl accelerationStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_ACCELEROMETER); - accelerometerChannel.setStreamHandler(accelerationStreamHandler); - - userAccelChannel = new EventChannel(messenger, USER_ACCELEROMETER_CHANNEL_NAME); - final StreamHandlerImpl linearAccelerationStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_LINEAR_ACCELERATION); - userAccelChannel.setStreamHandler(linearAccelerationStreamHandler); - - gyroscopeChannel = new EventChannel(messenger, GYROSCOPE_CHANNEL_NAME); - final StreamHandlerImpl gyroScopeStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_GYROSCOPE); - gyroscopeChannel.setStreamHandler(gyroScopeStreamHandler); - } - - private void teardownEventChannels() { - accelerometerChannel.setStreamHandler(null); - userAccelChannel.setStreamHandler(null); - gyroscopeChannel.setStreamHandler(null); - } -} diff --git a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java b/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java deleted file mode 100644 index 7e6da156386d..000000000000 --- a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java +++ /dev/null @@ -1,50 +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.sensors; - -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import io.flutter.plugin.common.EventChannel; - -class StreamHandlerImpl implements EventChannel.StreamHandler { - - private SensorEventListener sensorEventListener; - private final SensorManager sensorManager; - private final Sensor sensor; - - StreamHandlerImpl(SensorManager sensorManager, int sensorType) { - this.sensorManager = sensorManager; - sensor = sensorManager.getDefaultSensor(sensorType); - } - - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - sensorEventListener = createSensorEventListener(events); - sensorManager.registerListener(sensorEventListener, sensor, sensorManager.SENSOR_DELAY_NORMAL); - } - - @Override - public void onCancel(Object arguments) { - sensorManager.unregisterListener(sensorEventListener); - } - - SensorEventListener createSensorEventListener(final EventChannel.EventSink events) { - return new SensorEventListener() { - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) {} - - @Override - public void onSensorChanged(SensorEvent event) { - double[] sensorValues = new double[event.values.length]; - for (int i = 0; i < event.values.length; i++) { - sensorValues[i] = event.values[i]; - } - events.success(sensorValues); - } - }; - } -} diff --git a/packages/sensors/example/README.md b/packages/sensors/example/README.md deleted file mode 100644 index 9e7d7e0a76a9..000000000000 --- a/packages/sensors/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# sensors_example - -Demonstrates how to use the sensors plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/sensors/example/android.iml b/packages/sensors/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/sensors/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/sensors/example/android/app/build.gradle b/packages/sensors/example/android/app/build.gradle deleted file mode 100644 index c5dd855a64b7..000000000000 --- a/packages/sensors/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 31 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.sensorsexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/sensors/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/sensors/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/sensors/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/sensors/example/android/app/src/main/AndroidManifest.xml b/packages/sensors/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index ea3155cb9722..000000000000 --- a/packages/sensors/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java deleted file mode 100644 index 0f4298dca155..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java +++ /dev/null @@ -1,14 +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; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface DartIntegrationTest {} diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java deleted file mode 100644 index 52a6b8bebaf3..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java +++ /dev/null @@ -1,19 +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.sensorsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.plugins.DartIntegrationTest; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@DartIntegrationTest -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/sensors/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/sensors/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/sensors/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/sensors/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/sensors/example/android/build.gradle b/packages/sensors/example/android/build.gradle deleted file mode 100644 index e101ac08df55..000000000000 --- a/packages/sensors/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -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/sensors/example/android/gradle.properties b/packages/sensors/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/sensors/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/sensors/example/android/settings.gradle b/packages/sensors/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/sensors/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/packages/sensors/example/integration_test/sensors_test.dart b/packages/sensors/example/integration_test/sensors_test.dart deleted file mode 100644 index 3b8f614d2dcb..000000000000 --- a/packages/sensors/example/integration_test/sensors_test.dart +++ /dev/null @@ -1,26 +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. - -// @dart = 2.9 - -import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sensors/sensors.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can subscript to accelerometerEvents and get non-null events', - (WidgetTester tester) async { - final Completer completer = - Completer(); - StreamSubscription subscription; - subscription = accelerometerEvents.listen((AccelerometerEvent event) { - completer.complete(event); - subscription.cancel(); - }); - expect(await completer.future, isNotNull); - }); -} diff --git a/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist b/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/sensors/example/ios/Flutter/Debug.xcconfig b/packages/sensors/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/sensors/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/sensors/example/ios/Flutter/Release.xcconfig b/packages/sensors/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/sensors/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/sensors/example/ios/Podfile b/packages/sensors/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/sensors/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj b/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 69cd37f9ab86..000000000000 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,462 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 */; }; - A5B646543530B300A487D9B1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B16C8D77F2F0873936309F38 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 518BFCF6A33590E963FE1FA9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 65D7779632A59CFED1723B85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 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 = ""; }; - B16C8D77F2F0873936309F38 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A5B646543530B300A487D9B1 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5B101E38E51195F91ACE826E /* Pods */ = { - isa = PBXGroup; - children = ( - 65D7779632A59CFED1723B85 /* Pods-Runner.debug.xcconfig */, - 518BFCF6A33590E963FE1FA9 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 5B101E38E51195F91ACE826E /* Pods */, - DEA20432CDDA0D695086BE46 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - DEA20432CDDA0D695086BE46 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B16C8D77F2F0873936309F38 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 7B77DB2BA78582CC43C8E79F /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 7B77DB2BA78582CC43C8E79F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sensorsExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sensorsExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6254f..000000000000 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/sensors/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/sensors/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/sensors/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/sensors/example/ios/Runner/AppDelegate.h b/packages/sensors/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 0681d288bb70..000000000000 --- a/packages/sensors/example/ios/Runner/AppDelegate.h +++ /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. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/sensors/example/ios/Runner/AppDelegate.m b/packages/sensors/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 30b87969f44a..000000000000 --- a/packages/sensors/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +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. - -#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/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/sensors/example/ios/Runner/Base.lproj/Main.storyboard b/packages/sensors/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/sensors/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/sensors/example/ios/Runner/Info.plist b/packages/sensors/example/ios/Runner/Info.plist deleted file mode 100644 index bc49e9088995..000000000000 --- a/packages/sensors/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - sensors_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/sensors/example/ios/Runner/main.m b/packages/sensors/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/sensors/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/sensors/example/lib/main.dart b/packages/sensors/example/lib/main.dart deleted file mode 100644 index 0946a8e8421b..000000000000 --- a/packages/sensors/example/lib/main.dart +++ /dev/null @@ -1,144 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:sensors/sensors.dart'; - -import 'snake.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Sensors Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - static const int _snakeRows = 20; - static const int _snakeColumns = 20; - static const double _snakeCellSize = 10.0; - - List? _accelerometerValues; - List? _userAccelerometerValues; - List? _gyroscopeValues; - List> _streamSubscriptions = - >[]; - - @override - Widget build(BuildContext context) { - final List? accelerometer = - _accelerometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); - final List? gyroscope = - _gyroscopeValues?.map((double v) => v.toStringAsFixed(1)).toList(); - final List? userAccelerometer = _userAccelerometerValues - ?.map((double v) => v.toStringAsFixed(1)) - .toList(); - - return Scaffold( - appBar: AppBar( - title: const Text('Sensor Example'), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all(width: 1.0, color: Colors.black38), - ), - child: SizedBox( - height: _snakeRows * _snakeCellSize, - width: _snakeColumns * _snakeCellSize, - child: Snake( - rows: _snakeRows, - columns: _snakeColumns, - cellSize: _snakeCellSize, - ), - ), - ), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Accelerometer: $accelerometer'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('UserAccelerometer: $userAccelerometer'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Gyroscope: $gyroscope'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - ], - ), - ); - } - - @override - void dispose() { - super.dispose(); - for (StreamSubscription subscription in _streamSubscriptions) { - subscription.cancel(); - } - } - - @override - void initState() { - super.initState(); - _streamSubscriptions - .add(accelerometerEvents.listen((AccelerometerEvent event) { - setState(() { - _accelerometerValues = [event.x, event.y, event.z]; - }); - })); - _streamSubscriptions.add(gyroscopeEvents.listen((GyroscopeEvent event) { - setState(() { - _gyroscopeValues = [event.x, event.y, event.z]; - }); - })); - _streamSubscriptions - .add(userAccelerometerEvents.listen((UserAccelerometerEvent event) { - setState(() { - _userAccelerometerValues = [event.x, event.y, event.z]; - }); - })); - } -} diff --git a/packages/sensors/example/lib/snake.dart b/packages/sensors/example/lib/snake.dart deleted file mode 100644 index 47177681020f..000000000000 --- a/packages/sensors/example/lib/snake.dart +++ /dev/null @@ -1,129 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:sensors/sensors.dart'; - -class Snake extends StatefulWidget { - Snake({this.rows = 20, this.columns = 20, this.cellSize = 10.0}) { - assert(10 <= rows); - assert(10 <= columns); - assert(5.0 <= cellSize); - } - - final int rows; - final int columns; - final double cellSize; - - @override - State createState() => SnakeState(rows, columns, cellSize); -} - -class SnakeBoardPainter extends CustomPainter { - SnakeBoardPainter(this.state, this.cellSize); - - GameState state; - double cellSize; - - @override - void paint(Canvas canvas, Size size) { - final Paint blackLine = Paint()..color = Colors.black; - final Paint blackFilled = Paint() - ..color = Colors.black - ..style = PaintingStyle.fill; - canvas.drawRect( - Rect.fromPoints(Offset.zero, size.bottomLeft(Offset.zero)), - blackLine, - ); - for (math.Point p in state.body) { - final Offset a = Offset(cellSize * p.x, cellSize * p.y); - final Offset b = Offset(cellSize * (p.x + 1), cellSize * (p.y + 1)); - - canvas.drawRect(Rect.fromPoints(a, b), blackFilled); - } - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} - -class SnakeState extends State { - SnakeState(int rows, int columns, this.cellSize) - : state = GameState(rows, columns); - - double cellSize; - GameState state; - AccelerometerEvent? acceleration; - late StreamSubscription _streamSubscription; - late Timer _timer; - - @override - Widget build(BuildContext context) { - return CustomPaint(painter: SnakeBoardPainter(state, cellSize)); - } - - @override - void dispose() { - super.dispose(); - _streamSubscription.cancel(); - _timer.cancel(); - } - - @override - void initState() { - super.initState(); - _streamSubscription = - accelerometerEvents.listen((AccelerometerEvent event) { - setState(() { - acceleration = event; - }); - }); - - _timer = Timer.periodic(const Duration(milliseconds: 200), (_) { - setState(() { - _step(); - }); - }); - } - - void _step() { - final AccelerometerEvent? currentAcceleration = acceleration; - final math.Point? newDirection = currentAcceleration == null - ? null - : currentAcceleration.x.abs() < 1.0 && currentAcceleration.y.abs() < 1.0 - ? null - : (currentAcceleration.x.abs() < currentAcceleration.y.abs()) - ? math.Point(0, currentAcceleration.y.sign.toInt()) - : math.Point(-currentAcceleration.x.sign.toInt(), 0); - state.step(newDirection); - } -} - -class GameState { - GameState(this.rows, this.columns) - : snakeLength = math.min(rows, columns) - 5; - - int rows; - int columns; - int snakeLength; - - List> body = >[const math.Point(0, 0)]; - math.Point direction = const math.Point(1, 0); - - void step(math.Point? newDirection) { - math.Point next = body.last + direction; - next = math.Point(next.x % columns, next.y % rows); - - body.add(next); - if (body.length > snakeLength) body.removeAt(0); - direction = newDirection ?? direction; - } -} diff --git a/packages/sensors/example/pubspec.yaml b/packages/sensors/example/pubspec.yaml deleted file mode 100644 index fee7bd61f736..000000000000 --- a/packages/sensors/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: sensors_example -description: Demonstrates how to use the sensors plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - sensors: - # When depending on this package from a real application you should use: - # sensors: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/sensors/example/sensor_example.iml b/packages/sensors/example/sensor_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/sensors/example/sensor_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/sensors/example/sensor_example_android.iml b/packages/sensors/example/sensor_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/sensors/example/sensor_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/sensors/example/test_driver/integration_test.dart b/packages/sensors/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/sensors/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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. - -// @dart=2.9 - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/sensors/ios/Assets/.gitkeep b/packages/sensors/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/sensors/ios/Classes/FLTSensorsPlugin.h b/packages/sensors/ios/Classes/FLTSensorsPlugin.h deleted file mode 100644 index 8c3176b42a44..000000000000 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTSensorsPlugin : NSObject -@end - -@interface FLTUserAccelStreamHandler : NSObject -@end - -@interface FLTAccelerometerStreamHandler : NSObject -@end - -@interface FLTGyroscopeStreamHandler : NSObject -@end diff --git a/packages/sensors/ios/Classes/FLTSensorsPlugin.m b/packages/sensors/ios/Classes/FLTSensorsPlugin.m deleted file mode 100644 index 3d0ce66a2b25..000000000000 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.m +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTSensorsPlugin.h" -#import - -@implementation FLTSensorsPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTAccelerometerStreamHandler* accelerometerStreamHandler = - [[FLTAccelerometerStreamHandler alloc] init]; - FlutterEventChannel* accelerometerChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/accelerometer" - binaryMessenger:[registrar messenger]]; - [accelerometerChannel setStreamHandler:accelerometerStreamHandler]; - - FLTUserAccelStreamHandler* userAccelerometerStreamHandler = - [[FLTUserAccelStreamHandler alloc] init]; - FlutterEventChannel* userAccelerometerChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/user_accel" - binaryMessenger:[registrar messenger]]; - [userAccelerometerChannel setStreamHandler:userAccelerometerStreamHandler]; - - FLTGyroscopeStreamHandler* gyroscopeStreamHandler = [[FLTGyroscopeStreamHandler alloc] init]; - FlutterEventChannel* gyroscopeChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/gyroscope" - binaryMessenger:[registrar messenger]]; - [gyroscopeChannel setStreamHandler:gyroscopeStreamHandler]; -} - -@end - -const double GRAVITY = 9.8; -CMMotionManager* _motionManager; - -void _initMotionManager(void) { - if (!_motionManager) { - _motionManager = [[CMMotionManager alloc] init]; - } -} - -static void sendTriplet(Float64 x, Float64 y, Float64 z, FlutterEventSink sink) { - NSMutableData* event = [NSMutableData dataWithCapacity:3 * sizeof(Float64)]; - [event appendBytes:&x length:sizeof(Float64)]; - [event appendBytes:&y length:sizeof(Float64)]; - [event appendBytes:&z length:sizeof(Float64)]; - sink([FlutterStandardTypedData typedDataWithFloat64:event]); -} - -@implementation FLTAccelerometerStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startAccelerometerUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMAccelerometerData* accelerometerData, NSError* error) { - CMAcceleration acceleration = accelerometerData.acceleration; - // Multiply by gravity, and adjust sign values to - // align with Android. - sendTriplet(-acceleration.x * GRAVITY, -acceleration.y * GRAVITY, - -acceleration.z * GRAVITY, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopAccelerometerUpdates]; - return nil; -} - -@end - -@implementation FLTUserAccelStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startDeviceMotionUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMDeviceMotion* data, NSError* error) { - CMAcceleration acceleration = data.userAcceleration; - // Multiply by gravity, and adjust sign values to align with Android. - sendTriplet(-acceleration.x * GRAVITY, -acceleration.y * GRAVITY, - -acceleration.z * GRAVITY, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopDeviceMotionUpdates]; - return nil; -} - -@end - -@implementation FLTGyroscopeStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startGyroUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMGyroData* gyroData, NSError* error) { - CMRotationRate rotationRate = gyroData.rotationRate; - sendTriplet(rotationRate.x, rotationRate.y, rotationRate.z, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopGyroUpdates]; - return nil; -} - -@end diff --git a/packages/sensors/ios/sensors.podspec b/packages/sensors/ios/sensors.podspec deleted file mode 100644 index 0f0a4be29b0d..000000000000 --- a/packages/sensors/ios/sensors.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'sensors' - s.version = '0.0.1' - s.summary = 'Flutter Sensors' - s.description = <<-DESC -A Flutter plugin to access the accelerometer and gyroscope sensors. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/sensors' } - s.documentation_url = 'https://pub.dev/packages/sensors' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/sensors/lib/sensors.dart b/packages/sensors/lib/sensors.dart deleted file mode 100644 index 8db29e017ad0..000000000000 --- a/packages/sensors/lib/sensors.dart +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:flutter/services.dart'; - -const EventChannel _accelerometerEventChannel = - EventChannel('plugins.flutter.io/sensors/accelerometer'); - -const EventChannel _userAccelerometerEventChannel = - EventChannel('plugins.flutter.io/sensors/user_accel'); - -const EventChannel _gyroscopeEventChannel = - EventChannel('plugins.flutter.io/sensors/gyroscope'); - -/// Discrete reading from an accelerometer. Accelerometers measure the velocity -/// of the device. Note that these readings include the effects of gravity. Put -/// simply, you can use accelerometer readings to tell if the device is moving in -/// a particular direction. -class AccelerometerEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - AccelerometerEvent(this.x, this.y, this.z); - - /// Acceleration force along the x axis (including gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving to the right and negative mean it is moving to the left. - final double x; - - /// Acceleration force along the y axis (including gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving towards the sky and negative mean it is moving towards - /// the ground. - final double y; - - /// Acceleration force along the z axis (including gravity) measured in m/s^2. - /// - /// This uses a right-handed coordinate system. So when the device is held - /// upright and facing the user, positive values mean the device is moving - /// towards the user and negative mean it is moving away from them. - final double z; - - @override - String toString() => '[AccelerometerEvent (x: $x, y: $y, z: $z)]'; -} - -/// Discrete reading from a gyroscope. Gyroscopes measure the rate or rotation of -/// the device in 3D space. -class GyroscopeEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - GyroscopeEvent(this.x, this.y, this.z); - - /// Rate of rotation around the x axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "pitch". The top of the device will tilt towards or away from the - /// user as this value changes. - final double x; - - /// Rate of rotation around the y axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "yaw". The lengthwise edge of the device will rotate towards or away from - /// the user as this value changes. - final double y; - - /// Rate of rotation around the z axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "roll". When this changes the face of the device should remain facing - /// forward, but the orientation will change from portrait to landscape and so - /// on. - final double z; - - @override - String toString() => '[GyroscopeEvent (x: $x, y: $y, z: $z)]'; -} - -/// Like [AccelerometerEvent], this is a discrete reading from an accelerometer -/// and measures the velocity of the device. However, unlike -/// [AccelerometerEvent], this event does not include the effects of gravity. -class UserAccelerometerEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - UserAccelerometerEvent(this.x, this.y, this.z); - - /// Acceleration force along the x axis (excluding gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving to the right and negative mean it is moving to the left. - final double x; - - /// Acceleration force along the y axis (excluding gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving towards the sky and negative mean it is moving towards - /// the ground. - final double y; - - /// Acceleration force along the z axis (excluding gravity) measured in m/s^2. - /// - /// This uses a right-handed coordinate system. So when the device is held - /// upright and facing the user, positive values mean the device is moving - /// towards the user and negative mean it is moving away from them. - final double z; - - @override - String toString() => '[UserAccelerometerEvent (x: $x, y: $y, z: $z)]'; -} - -AccelerometerEvent _listToAccelerometerEvent(List list) { - return AccelerometerEvent(list[0], list[1], list[2]); -} - -UserAccelerometerEvent _listToUserAccelerometerEvent(List list) { - return UserAccelerometerEvent(list[0], list[1], list[2]); -} - -GyroscopeEvent _listToGyroscopeEvent(List list) { - return GyroscopeEvent(list[0], list[1], list[2]); -} - -Stream? _accelerometerEvents; -Stream? _gyroscopeEvents; -Stream? _userAccelerometerEvents; - -/// A broadcast stream of events from the device accelerometer. -Stream get accelerometerEvents { - Stream? accelerometerEvents = _accelerometerEvents; - if (accelerometerEvents == null) { - accelerometerEvents = - _accelerometerEventChannel.receiveBroadcastStream().map( - (dynamic event) => - _listToAccelerometerEvent(event.cast()), - ); - _accelerometerEvents = accelerometerEvents; - } - - return accelerometerEvents; -} - -/// A broadcast stream of events from the device gyroscope. -Stream get gyroscopeEvents { - Stream? gyroscopeEvents = _gyroscopeEvents; - if (gyroscopeEvents == null) { - gyroscopeEvents = _gyroscopeEventChannel.receiveBroadcastStream().map( - (dynamic event) => _listToGyroscopeEvent(event.cast()), - ); - _gyroscopeEvents = gyroscopeEvents; - } - - return gyroscopeEvents; -} - -/// Events from the device accelerometer with gravity removed. -Stream get userAccelerometerEvents { - Stream? userAccelerometerEvents = - _userAccelerometerEvents; - if (userAccelerometerEvents == null) { - userAccelerometerEvents = - _userAccelerometerEventChannel.receiveBroadcastStream().map( - (dynamic event) => - _listToUserAccelerometerEvent(event.cast()), - ); - _userAccelerometerEvents = userAccelerometerEvents; - } - - return userAccelerometerEvents; -} diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml deleted file mode 100644 index b26819b64df0..000000000000 --- a/packages/sensors/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: sensors -description: Flutter plugin for accessing the Android and iOS accelerometer and - gyroscope sensors. -repository: https://github.com/flutter/plugins/tree/master/packages/sensors -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+sensors%22 -version: 2.0.3 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.sensors - pluginClass: SensorsPlugin - ios: - pluginClass: FLTSensorsPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - test: ^1.16.0 - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/sensors/test/sensors_test.dart b/packages/sensors/test/sensors_test.dart deleted file mode 100644 index bce3afe6205b..000000000000 --- a/packages/sensors/test/sensors_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sensors/sensors.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('$accelerometerEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/accelerometer'; - const List sensorData = [1.0, 2.0, 3.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final AccelerometerEvent event = await accelerometerEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); - - test('$gyroscopeEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/gyroscope'; - const List sensorData = [3.0, 4.0, 5.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final GyroscopeEvent event = await gyroscopeEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); - - test('$userAccelerometerEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/user_accel'; - const List sensorData = [6.0, 7.0, 8.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final UserAccelerometerEvent event = await userAccelerometerEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); -} - -void _initializeFakeSensorChannel(String channelName, List sensorData) { - const StandardMethodCodec standardMethod = StandardMethodCodec(); - - void _emitEvent(ByteData? event) { - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channelName, - event, - (ByteData? reply) {}, - ); - } - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .setMockMessageHandler(channelName, (ByteData? message) async { - final MethodCall methodCall = standardMethod.decodeMethodCall(message); - if (methodCall.method == 'listen') { - _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); - _emitEvent(null); - return standardMethod.encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - return standardMethod.encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }); -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/share/AUTHORS b/packages/share/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/share/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md deleted file mode 100644 index effc12c023c0..000000000000 --- a/packages/share/CHANGELOG.md +++ /dev/null @@ -1,225 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. -* Updates Android compileSdkVersion to 31. - -## 2.0.4 - -* Update README to point to Plus Plugins version. - -## 2.0.3 - -* Do not tear down method channel onDetachedFromActivity. - -## 2.0.2 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.1 - -* Migrate unit tests to sound null safety. - -## 2.0.0 - -* Migrate to null safety. -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) -* Update README with the new documentation urls. - -## 0.6.5+5 - -* Update Flutter SDK constraint. - -## 0.6.5+4 - -* Fix iPad share window not showing when `origin` is null. - -## 0.6.5+3 - -* Replace deprecated `Environment.getExternalStorageDirectory()` call on Android. -* Upgrade to Android Gradle plugin 3.5.0 & target API level 29. - -## 0.6.5+2 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.6.5+1 - -* Avoiding uses unchecked or unsafe Object Type Casting - -## 0.6.5 - -* Added support for sharing files - -## 0.6.4+5 - -* Update package:e2e -> package:integration_test - -## 0.6.4+4 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.6.4+3 - -* Post-v2 Android embedding cleanup. - -## 0.6.4+2 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.6.4+1 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.6.4 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. -* Fix CocoaPods podspec lint warnings. - -## 0.6.3+8 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.6.3+7 - -* Updated gradle version of example. - -## 0.6.3+6 - -* Make the pedantic dev_dependency explicit. - -## 0.6.3+5 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.6.3+4 - -* Fix pedantic lints. This shouldn't affect existing functionality. - -## 0.6.3+3 - -* README update. - -## 0.6.3+2 - -* Remove AndroidX warnings. - -## 0.6.3+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.6.3 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - -## 0.6.2+4 - -* Define clang module for iOS. - -## 0.6.2+3 - -* Fix iOS crash when setting subject to null. - -## 0.6.2+2 - -* Update and migrate iOS example project. - -## 0.6.2+1 - -* Specify explicit type for `invokeMethod`. -* Use `const` for `Rect`. -* Updated minimum Flutter SDK to 1.6.0. - -## 0.6.2 - -* Add optional subject to fill email subject in case user selects email app. - -## 0.6.1+2 - -* Update Dart code to conform to current Dart formatter. - -## 0.6.1+1 - -* Fix analyzer warnings about `const Rect` in tests. - -## 0.6.1 - -* Updated Android compileSdkVersion to 28 to match other plugins. - -## 0.6.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.6.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.5.3 - -* Added missing test package dependency. -* Bumped version of mockito package dependency to pick up Dart 2 support. - -## 0.5.2 - -* Fixes iOS sharing - -## 0.5.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.5.0 - -* **Breaking change**. Namespaced the `share` method inside a `Share` class. -* Fixed crash when sharing on iPad. -* Added functionality to specify share sheet origin on iOS. - -## 0.4.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.3.2 - -* Fixed Dart 2 type error. - -## 0.3.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.3.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.2.2 - -* Added FLT prefix to iOS types - -## 0.2.1 - -* Updated README -* Bumped buildToolsVersion to 25.0.3 - -## 0.2.0 - -* Upgrade to new plugin registration. (https://groups.google.com/forum/#!topic/flutter-dev/zba1Ynf2OKM) - -## 0.1.0 - -* Initial Open Source release. diff --git a/packages/share/LICENSE b/packages/share/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/share/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/share/README.md b/packages/share/README.md deleted file mode 100644 index 7fda1198f503..000000000000 --- a/packages/share/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Share plugin - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`share_plus`](https://pub.dev/packages/share_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -[![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) - -A Flutter plugin to share content from your Flutter app via the platform's -share dialog. - -Wraps the ACTION_SEND Intent on Android and UIActivityViewController -on iOS. - -## Usage - -To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages/). - -## Example - -Import the library. - -``` dart -import 'package:share/share.dart'; -``` - -Then invoke the static `share` method anywhere in your Dart code. - -``` dart -Share.share('check out my website https://example.com'); -``` - -The `share` method also takes an optional `subject` that will be used when -sharing to email. - -``` dart -Share.share('check out my website https://example.com', subject: 'Look what I made!'); -``` - -To share one or multiple files invoke the static `shareFiles` method anywhere in your Dart code. Optionally you can also pass in `text` and `subject`. -``` dart -Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); -Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); -``` diff --git a/packages/share/analysis_options.yaml b/packages/share/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/share/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle deleted file mode 100644 index c4fcafc70d26..000000000000 --- a/packages/share/android/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -group 'io.flutter.plugins.share' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 31 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - dependencies { - implementation 'androidx.core:core:1.3.1' - implementation 'androidx.annotation:annotation:1.1.0' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/share/android/settings.gradle b/packages/share/android/settings.gradle deleted file mode 100644 index 64350ae697e7..000000000000 --- a/packages/share/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'share' diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml deleted file mode 100644 index c141a5c67928..000000000000 --- a/packages/share/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java deleted file mode 100644 index 7f162e883c32..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java +++ /dev/null @@ -1,59 +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.share; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.io.*; -import java.util.List; -import java.util.Map; - -/** Handles the method calls for the plugin. */ -class MethodCallHandler implements MethodChannel.MethodCallHandler { - - private Share share; - - MethodCallHandler(Share share) { - this.share = share; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "share": - expectMapArguments(call); - // Android does not support showing the share sheet at a particular point on screen. - String text = call.argument("text"); - String subject = call.argument("subject"); - share.share(text, subject); - result.success(null); - break; - case "shareFiles": - expectMapArguments(call); - - List paths = call.argument("paths"); - List mimeTypes = call.argument("mimeTypes"); - text = call.argument("text"); - subject = call.argument("subject"); - // Android does not support showing the share sheet at a particular point on screen. - try { - share.shareFiles(paths, mimeTypes, text, subject); - result.success(null); - } catch (IOException e) { - result.error(e.getMessage(), null, null); - } - break; - default: - result.notImplemented(); - break; - } - } - - private void expectMapArguments(MethodCall call) throws IllegalArgumentException { - if (!(call.arguments instanceof Map)) { - throw new IllegalArgumentException("Map argument expected"); - } - } -} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java deleted file mode 100644 index fced7bb7f87c..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java +++ /dev/null @@ -1,232 +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.share; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -/** Handles share intent. */ -class Share { - - private Context context; - private Activity activity; - - /** - * Constructs a Share object. The {@code context} and {@code activity} are used to start the share - * intent. The {@code activity} might be null when constructing the {@link Share} object and set - * to non-null when an activity is available using {@link #setActivity(Activity)}. - */ - Share(Context context, Activity activity) { - this.context = context; - this.activity = activity; - } - - /** - * Sets the activity when an activity is available. When the activity becomes unavailable, use - * this method to set it to null. - */ - void setActivity(Activity activity) { - this.activity = activity; - } - - void share(String text, String subject) { - if (text == null || text.isEmpty()) { - throw new IllegalArgumentException("Non-empty text expected"); - } - - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, text); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - shareIntent.setType("text/plain"); - Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); - startActivity(chooserIntent); - } - - void shareFiles(List paths, List mimeTypes, String text, String subject) - throws IOException { - if (paths == null || paths.isEmpty()) { - throw new IllegalArgumentException("Non-empty path expected"); - } - - clearExternalShareFolder(); - ArrayList fileUris = getUrisForPaths(paths); - - Intent shareIntent = new Intent(); - if (fileUris.isEmpty()) { - share(text, subject); - return; - } else if (fileUris.size() == 1) { - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); - shareIntent.setType( - !mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*"); - } else { - shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); - shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris); - shareIntent.setType(reduceMimeTypes(mimeTypes)); - } - if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); - if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); - - List resInfoList = - getContext() - .getPackageManager() - .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - for (Uri fileUri : fileUris) { - getContext() - .grantUriPermission( - packageName, - fileUri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - - startActivity(chooserIntent); - } - - private void startActivity(Intent intent) { - if (activity != null) { - activity.startActivity(intent); - } else if (context != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } else { - throw new IllegalStateException("Both context and activity are null"); - } - } - - private ArrayList getUrisForPaths(List paths) throws IOException { - ArrayList uris = new ArrayList<>(paths.size()); - for (String path : paths) { - File file = new File(path); - if (!fileIsOnExternal(file)) { - file = copyToExternalShareFolder(file); - } - - uris.add( - FileProvider.getUriForFile( - getContext(), getContext().getPackageName() + ".flutter.share_provider", file)); - } - return uris; - } - - private String reduceMimeTypes(List mimeTypes) { - if (mimeTypes.size() > 1) { - String reducedMimeType = mimeTypes.get(0); - for (int i = 1; i < mimeTypes.size(); i++) { - String mimeType = mimeTypes.get(i); - if (!reducedMimeType.equals(mimeType)) { - if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) { - reducedMimeType = getMimeTypeBase(mimeType) + "/*"; - } else { - reducedMimeType = "*/*"; - break; - } - } - } - return reducedMimeType; - } else if (mimeTypes.size() == 1) { - return mimeTypes.get(0); - } else { - return "*/*"; - } - } - - @NonNull - private String getMimeTypeBase(String mimeType) { - if (mimeType == null || !mimeType.contains("/")) { - return "*"; - } - - return mimeType.substring(0, mimeType.indexOf("/")); - } - - private boolean fileIsOnExternal(File file) { - try { - String filePath = file.getCanonicalPath(); - File externalDir = context.getExternalFilesDir(null); - return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath()); - } catch (IOException e) { - return false; - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - private void clearExternalShareFolder() { - File folder = getExternalShareFolder(); - if (folder.exists()) { - for (File file : folder.listFiles()) { - file.delete(); - } - folder.delete(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - private File copyToExternalShareFolder(File file) throws IOException { - File folder = getExternalShareFolder(); - if (!folder.exists()) { - folder.mkdirs(); - } - - File newFile = new File(folder, file.getName()); - copy(file, newFile); - return newFile; - } - - @NonNull - private File getExternalShareFolder() { - return new File(getContext().getExternalCacheDir(), "share"); - } - - private Context getContext() { - if (activity != null) { - return activity; - } - if (context != null) { - return context; - } - - throw new IllegalStateException("Both context and activity are null"); - } - - private static void copy(File src, File dst) throws IOException { - InputStream in = new FileInputStream(src); - try { - OutputStream out = new FileOutputStream(dst); - try { - // Transfer bytes from in to out - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } finally { - out.close(); - } - } finally { - in.close(); - } - } -} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java deleted file mode 100644 index fff48a6bad14..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java +++ /dev/null @@ -1,14 +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.share; - -import androidx.core.content.FileProvider; - -/** - * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. - * - *

See https://developer.android.com/guide/topics/manifest/provider-element.html for details. - */ -public class ShareFileProvider extends FileProvider {} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java deleted file mode 100644 index c596b8b71555..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java +++ /dev/null @@ -1,67 +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.share; - -import android.app.Activity; -import android.content.Context; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; - -/** Plugin method host for presenting a share sheet via Intent */ -public class SharePlugin implements FlutterPlugin, ActivityAware { - - private static final String CHANNEL = "plugins.flutter.io/share"; - private MethodCallHandler handler; - private Share share; - private MethodChannel methodChannel; - - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - SharePlugin plugin = new SharePlugin(); - plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - share = null; - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - share.setActivity(binding.getActivity()); - } - - @Override - public void onDetachedFromActivity() { - share.setActivity(null); - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - onAttachedToActivity(binding); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); - } - - private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, CHANNEL); - share = new Share(context, activity); - handler = new MethodCallHandler(share); - methodChannel.setMethodCallHandler(handler); - } -} diff --git a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml deleted file mode 100644 index e68bf916a30b..000000000000 --- a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/share/example/README.md b/packages/share/example/README.md deleted file mode 100644 index 4081c8a5c9c3..000000000000 --- a/packages/share/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# share_example - -Demonstrates how to use the share plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/share/example/android.iml b/packages/share/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/share/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/share/example/android/app/build.gradle b/packages/share/example/android/app/build.gradle deleted file mode 100644 index c0f1e05f2639..000000000000 --- a/packages/share/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 31 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.shareexample" - minSdkVersion 16 - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/share/example/android/app/src/main/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d1f1ce953e3a..000000000000 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java deleted file mode 100644 index 0f4298dca155..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java +++ /dev/null @@ -1,14 +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; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface DartIntegrationTest {} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java deleted file mode 100644 index aba658887d88..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java +++ /dev/null @@ -1,19 +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.shareexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.plugins.DartIntegrationTest; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@DartIntegrationTest -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/share/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/share/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/share/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/share/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/share/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/share/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/share/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/share/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/share/example/android/build.gradle b/packages/share/example/android/build.gradle deleted file mode 100644 index 456d020f6e2c..000000000000 --- a/packages/share/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/share/example/android/gradle.properties b/packages/share/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/share/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d757f3d33fcc..000000000000 --- a/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/share/example/android/settings.gradle b/packages/share/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/share/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/packages/share/example/integration_test/share_test.dart b/packages/share/example/integration_test/share_test.dart deleted file mode 100644 index 54d553bbb5a0..000000000000 --- a/packages/share/example/integration_test/share_test.dart +++ /dev/null @@ -1,17 +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. - -// @dart = 2.9 - -import 'package:flutter_test/flutter_test.dart'; -import 'package:share/share.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can launch share', (WidgetTester tester) async { - expect(Share.share('message', subject: 'title'), completes); - }); -} diff --git a/packages/share/example/ios/Flutter/AppFrameworkInfo.plist b/packages/share/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/share/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/share/example/ios/Flutter/Debug.xcconfig b/packages/share/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/share/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/share/example/ios/Flutter/Release.xcconfig b/packages/share/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/share/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/share/example/ios/Podfile b/packages/share/example/ios/Podfile deleted file mode 100644 index 3924e59aa0f9..000000000000 --- a/packages/share/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index d7e896212533..000000000000 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,698 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 85392794417D70A970945C83 /* libPods-Runner.a */; }; - 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */; }; - 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B4226EFCEF400A4A191 /* RunnerTests.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 683426AD2538D314009B194C /* FLTShareExampleUITests.m */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 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 */ - 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - 683426B02538D314009B194C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 002F2AAB9479773692FEF066 /* 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 = ""; }; - 1BCE6CBBA2E91FD0397A29C8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33E20B4226EFCEF400A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; - 33E20B4426EFCEF400A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 683426AB2538D314009B194C /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 683426AD2538D314009B194C /* FLTShareExampleUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTShareExampleUITests.m; sourceTree = ""; }; - 683426AF2538D314009B194C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 85392794417D70A970945C83 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33E20B3D26EFCEF400A4A191 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A82538D314009B194C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 16DDF472245BCC3E62219493 /* Pods */ = { - isa = PBXGroup; - children = ( - 1BCE6CBBA2E91FD0397A29C8 /* Pods-Runner.debug.xcconfig */, - 002F2AAB9479773692FEF066 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 33E20B4126EFCEF400A4A191 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 33E20B4226EFCEF400A4A191 /* RunnerTests.m */, - 33E20B4426EFCEF400A4A191 /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 683426AC2538D314009B194C /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - 683426AD2538D314009B194C /* FLTShareExampleUITests.m */, - 683426AF2538D314009B194C /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; - 8CA31EF57239BF20619316D9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 85392794417D70A970945C83 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 683426AC2538D314009B194C /* RunnerUITests */, - 33E20B4126EFCEF400A4A191 /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - 16DDF472245BCC3E62219493 /* Pods */, - 8CA31EF57239BF20619316D9 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 683426AB2538D314009B194C /* RunnerUITests.xctest */, - 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */, - 2D9222501EC45DE6007564B0 /* 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 = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33E20B3F26EFCEF400A4A191 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 33E20B3C26EFCEF400A4A191 /* Sources */, - 33E20B3D26EFCEF400A4A191 /* Frameworks */, - 33E20B3E26EFCEF400A4A191 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 683426AA2538D314009B194C /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - 683426A72538D314009B194C /* Sources */, - 683426A82538D314009B194C /* Frameworks */, - 683426A92538D314009B194C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 683426B12538D314009B194C /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = 683426AB2538D314009B194C /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 5F8AC0B5B699C537B657C107 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 33E20B3F26EFCEF400A4A191 = { - CreatedOnToolsVersion = 12.5; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 683426AA2538D314009B194C = { - CreatedOnToolsVersion = 11.7; - 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 */, - 683426AA2538D314009B194C /* RunnerUITests */, - 33E20B3F26EFCEF400A4A191 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33E20B3E26EFCEF400A4A191 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A92538D314009B194C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - 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"; - }; - 5F8AC0B5B699C537B657C107 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33E20B3C26EFCEF400A4A191 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A72538D314009B194C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */; - }; - 683426B12538D314009B194C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 683426B02538D314009B194C /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - 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 */ - 33E20B4726EFCEF400A4A191 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 33E20B4826EFCEF400A4A191 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; - 683426B22538D314009B194C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - 683426B32538D314009B194C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.shareExample; - 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.shareExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33E20B4726EFCEF400A4A191 /* Debug */, - 33E20B4826EFCEF400A4A191 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 683426B22538D314009B194C /* Debug */, - 683426B32538D314009B194C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 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/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6254f..000000000000 --- a/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/share/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/share/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/share/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/share/example/ios/Runner/AppDelegate.h b/packages/share/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 0681d288bb70..000000000000 --- a/packages/share/example/ios/Runner/AppDelegate.h +++ /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. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/share/example/ios/Runner/AppDelegate.m b/packages/share/example/ios/Runner/AppDelegate.m deleted file mode 100644 index b790a0a52635..000000000000 --- a/packages/share/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,16 +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. - -#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/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/share/example/ios/Runner/Base.lproj/Main.storyboard b/packages/share/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/share/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/share/example/ios/Runner/Info.plist b/packages/share/example/ios/Runner/Info.plist deleted file mode 100644 index 71656105a1fa..000000000000 --- a/packages/share/example/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - share_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 - - NSPhotoLibraryUsageDescription - This app requires access to the photo library for sharing images. - NSMicrophoneUsageDescription - This app does not require access to the microphone for sharing images. - NSCameraUsageDescription - This app requires access to the camera for sharing images. - - diff --git a/packages/share/example/ios/Runner/main.m b/packages/share/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/share/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/share/example/ios/RunnerTests/Info.plist b/packages/share/example/ios/RunnerTests/Info.plist deleted file mode 100644 index 64d65ca49577..000000000000 --- a/packages/share/example/ios/RunnerTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/packages/share/example/ios/RunnerTests/RunnerTests.m b/packages/share/example/ios/RunnerTests/RunnerTests.m deleted file mode 100644 index 3c4c341fd451..000000000000 --- a/packages/share/example/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import share; -@import XCTest; - -@interface ShareTests : XCTestCase -@end - -@implementation ShareTests - -- (void)testPlugin { - FLTSharePlugin* plugin = [[FLTSharePlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m b/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m deleted file mode 100644 index c099cb946b92..000000000000 --- a/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -static const NSInteger kSecondsToWaitWhenFindingElements = 30; - -@interface FLTShareExampleUITests : XCTestCase - -@end - -@implementation FLTShareExampleUITests - -- (void)setUp { - self.continueAfterFailure = NO; -} - -- (void)testShareWithEmptyOrigin { - XCUIApplication* app = [[XCUIApplication alloc] init]; - [app launch]; - - XCUIElement* shareWithEmptyOriginButton = [app.buttons - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"label == %@", @"Share With Empty Origin"]]; - if (![shareWithEmptyOriginButton waitForExistenceWithTimeout:kSecondsToWaitWhenFindingElements]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find shareWithEmptyOriginButton with %@ seconds", - @(kSecondsToWaitWhenFindingElements)); - } - - XCTAssertNotNil(shareWithEmptyOriginButton); - [shareWithEmptyOriginButton tap]; - - // Find the share popup. - XCUIElement* activityListView = [app.otherElements - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"identifier == %@", @"ActivityListView"]]; - if (![activityListView waitForExistenceWithTimeout:kSecondsToWaitWhenFindingElements]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find activityListView with %@ seconds", - @(kSecondsToWaitWhenFindingElements)); - } - XCTAssertNotNil(activityListView); -} - -@end diff --git a/packages/share/example/ios/RunnerUITests/Info.plist b/packages/share/example/ios/RunnerUITests/Info.plist deleted file mode 100644 index 64d65ca49577..000000000000 --- a/packages/share/example/ios/RunnerUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart deleted file mode 100644 index 9b5b807c77c6..000000000000 --- a/packages/share/example/lib/image_previews.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -/// Widget for displaying a preview of images -class ImagePreviews extends StatelessWidget { - /// The image paths of the displayed images - final List imagePaths; - - /// Callback when an image should be removed - final Function(int)? onDelete; - - /// Creates a widget for preview of images. [imagePaths] can not be empty - /// and all contained paths need to be non empty. - const ImagePreviews(this.imagePaths, {Key? key, this.onDelete}) - : super(key: key); - - @override - Widget build(BuildContext context) { - if (imagePaths.isEmpty) { - return Container(); - } - - List imageWidgets = []; - for (int i = 0; i < imagePaths.length; i++) { - imageWidgets.add(_ImagePreview( - imagePaths[i], - onDelete: onDelete != null ? () => onDelete!(i) : null, - )); - } - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: imageWidgets), - ); - } -} - -class _ImagePreview extends StatelessWidget { - final String imagePath; - final VoidCallback? onDelete; - - const _ImagePreview(this.imagePath, {Key? key, this.onDelete}) - : super(key: key); - - @override - Widget build(BuildContext context) { - File imageFile = File(imagePath); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 200, - maxHeight: 200, - ), - child: Image.file(imageFile), - ), - Positioned( - right: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: FloatingActionButton( - backgroundColor: Colors.red, - child: Icon(Icons.delete), - onPressed: onDelete), - ), - ), - ], - ), - ); - } -} diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart deleted file mode 100644 index f802b492df6d..000000000000 --- a/packages/share/example/lib/main.dart +++ /dev/null @@ -1,136 +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. - -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:share/share.dart'; - -import 'image_previews.dart'; - -void main() { - runApp(DemoApp()); -} - -class DemoApp extends StatefulWidget { - @override - DemoAppState createState() => DemoAppState(); -} - -class DemoAppState extends State { - String text = ''; - String subject = ''; - List imagePaths = []; - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Share Plugin Demo', - home: Scaffold( - appBar: AppBar( - title: const Text('Share Plugin Demo'), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - decoration: const InputDecoration( - labelText: 'Share text:', - hintText: 'Enter some text and/or link to share', - ), - maxLines: 2, - onChanged: (String value) => setState(() { - text = value; - }), - ), - TextField( - decoration: const InputDecoration( - labelText: 'Share subject:', - hintText: 'Enter subject to share (optional)', - ), - maxLines: 2, - onChanged: (String value) => setState(() { - subject = value; - }), - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - ImagePreviews(imagePaths, onDelete: _onDeleteImage), - ListTile( - leading: Icon(Icons.add), - title: Text("Add image"), - onTap: () async { - final imagePicker = ImagePicker(); - final pickedFile = await imagePicker.getImage( - source: ImageSource.gallery, - ); - if (pickedFile != null) { - setState(() { - imagePaths.add(pickedFile.path); - }); - } - }, - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - Builder( - builder: (BuildContext context) { - return ElevatedButton( - child: const Text('Share'), - onPressed: text.isEmpty && imagePaths.isEmpty - ? null - : () => _onShare(context), - ); - }, - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - Builder( - builder: (BuildContext context) { - return ElevatedButton( - child: const Text('Share With Empty Origin'), - onPressed: () => _onShareWithEmptyOrigin(context), - ); - }, - ), - ], - ), - ), - )), - ); - } - - _onDeleteImage(int position) { - setState(() { - imagePaths.removeAt(position); - }); - } - - _onShare(BuildContext context) async { - // A builder is used to retrieve the context immediately - // surrounding the ElevatedButton. - // - // The context's `findRenderObject` returns the first - // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The ElevatedButton's RenderObject - // has its position and size after it's built. - final RenderBox box = context.findRenderObject() as RenderBox; - - if (imagePaths.isNotEmpty) { - await Share.shareFiles(imagePaths, - text: text, - subject: subject, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); - } else { - await Share.share(text, - subject: subject, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); - } - } - - _onShareWithEmptyOrigin(BuildContext context) async { - await Share.share("text"); - } -} diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml deleted file mode 100644 index 8a28b43d46e4..000000000000 --- a/packages/share/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: share_example -description: Demonstrates how to use the share plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2" - -dependencies: - flutter: - sdk: flutter - share: - # When depending on this package from a real application you should use: - # share: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - image_picker: ^0.7.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/share/example/share_example.iml b/packages/share/example/share_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/share/example/share_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/share/example/test_driver/integration_test.dart b/packages/share/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/share/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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. - -// @dart=2.9 - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/share/ios/Assets/.gitkeep b/packages/share/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/share/ios/Classes/FLTSharePlugin.h b/packages/share/ios/Classes/FLTSharePlugin.h deleted file mode 100644 index 8f6a6a538e2a..000000000000 --- a/packages/share/ios/Classes/FLTSharePlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTSharePlugin : NSObject -@end diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m deleted file mode 100644 index b8a3a7ffa316..000000000000 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTSharePlugin.h" - -static NSString *const PLATFORM_CHANNEL = @"plugins.flutter.io/share"; - -@interface ShareData : NSObject - -@property(readonly, nonatomic, copy) NSString *subject; -@property(readonly, nonatomic, copy) NSString *text; -@property(readonly, nonatomic, copy) NSString *path; -@property(readonly, nonatomic, copy) NSString *mimeType; - -- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text NS_DESIGNATED_INITIALIZER; -- (instancetype)initWithFile:(NSString *)path - mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; - -- (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead"))); - -@end - -@implementation ShareData - -- (instancetype)init { - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { - self = [super init]; - if (self) { - _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; - _text = text; - } - return self; -} - -- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { - self = [super init]; - if (self) { - _path = path; - _mimeType = mimeType; - } - return self; -} - -- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { - return @""; -} - -- (id)activityViewController:(UIActivityViewController *)activityViewController - itemForActivityType:(UIActivityType)activityType { - if (!_path || !_mimeType) { - return _text; - } - - if ([_mimeType hasPrefix:@"image/"]) { - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return image; - } else { - NSURL *url = [NSURL fileURLWithPath:_path]; - return url; - } -} - -- (NSString *)activityViewController:(UIActivityViewController *)activityViewController - subjectForActivityType:(UIActivityType)activityType { - return _subject; -} - -- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController - thumbnailImageForActivityType:(UIActivityType)activityType - suggestedSize:(CGSize)suggestedSize { - if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { - return nil; - } - - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return [self imageWithImage:image scaledToSize:suggestedSize]; -} - -- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { - UIGraphicsBeginImageContext(newSize); - [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return newImage; -} - -@end - -@implementation FLTSharePlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *shareChannel = - [FlutterMethodChannel methodChannelWithName:PLATFORM_CHANNEL - binaryMessenger:registrar.messenger]; - - [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - NSDictionary *arguments = [call arguments]; - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; - - CGRect originRect = CGRectZero; - if (originX && originY && originWidth && originHeight) { - originRect = CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); - } - - if ([@"share" isEqualToString:call.method]) { - NSString *shareText = arguments[@"text"]; - NSString *shareSubject = arguments[@"subject"]; - - if (shareText.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty text expected" - details:nil]); - return; - } - - [self shareText:shareText - subject:shareSubject - withController:[UIApplication sharedApplication].keyWindow.rootViewController - atSource:originRect]; - result(nil); - } else if ([@"shareFiles" isEqualToString:call.method]) { - NSArray *paths = arguments[@"paths"]; - NSArray *mimeTypes = arguments[@"mimeTypes"]; - NSString *subject = arguments[@"subject"]; - NSString *text = arguments[@"text"]; - - if (paths.count == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty paths expected" - details:nil]); - return; - } - - for (NSString *path in paths) { - if (path.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Each path must not be empty" - details:nil]); - return; - } - } - - [self shareFiles:paths - withMimeType:mimeTypes - withSubject:subject - withText:text - withController:[UIApplication sharedApplication].keyWindow.rootViewController - atSource:originRect]; - result(nil); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -+ (void)share:(NSArray *)shareItems - withController:(UIViewController *)controller - atSource:(CGRect)origin { - UIActivityViewController *activityViewController = - [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; - activityViewController.popoverPresentationController.sourceView = controller.view; - activityViewController.popoverPresentationController.sourceRect = origin; - - [controller presentViewController:activityViewController animated:YES completion:nil]; -} - -+ (void)shareText:(NSString *)shareText - subject:(NSString *)subject - withController:(UIViewController *)controller - atSource:(CGRect)origin { - ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; - [self share:@[ data ] withController:controller atSource:origin]; -} - -+ (void)shareFiles:(NSArray *)paths - withMimeType:(NSArray *)mimeTypes - withSubject:(NSString *)subject - withText:(NSString *)text - withController:(UIViewController *)controller - atSource:(CGRect)origin { - NSMutableArray *items = [[NSMutableArray alloc] init]; - - if (text || subject) { - [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; - } - - for (int i = 0; i < [paths count]; i++) { - NSString *path = paths[i]; - NSString *pathExtension = [path pathExtension]; - NSString *mimeType = mimeTypes[i]; - if ([pathExtension.lowercaseString isEqualToString:@"jpg"] || - [pathExtension.lowercaseString isEqualToString:@"jpeg"] || - [pathExtension.lowercaseString isEqualToString:@"png"] || - [mimeType.lowercaseString isEqualToString:@"image/jpg"] || - [mimeType.lowercaseString isEqualToString:@"image/jpeg"] || - [mimeType.lowercaseString isEqualToString:@"image/png"]) { - UIImage *image = [UIImage imageWithContentsOfFile:path]; - [items addObject:image]; - } else { - [items addObject:[[ShareData alloc] initWithFile:path mimeType:mimeType]]; - } - } - - [self share:items withController:controller atSource:origin]; -} - -@end diff --git a/packages/share/ios/share.podspec b/packages/share/ios/share.podspec deleted file mode 100644 index 786e1c7fb922..000000000000 --- a/packages/share/ios/share.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'share' - s.version = '0.0.1' - s.summary = 'Flutter Share' - s.description = <<-DESC -A Flutter plugin to share content from your Flutter app via the platform's share dialog. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/share' } - s.documentation_url = 'https://pub.dev/packages/share' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart deleted file mode 100644 index 6a3f5a317e31..000000000000 --- a/packages/share/lib/share.dart +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:ui'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show visibleForTesting; -import 'package:mime/mime.dart' show lookupMimeType; - -/// Plugin for summoning a platform share sheet. -class Share { - /// [MethodChannel] used to communicate with the platform side. - @visibleForTesting - static const MethodChannel channel = - MethodChannel('plugins.flutter.io/share'); - - /// Summons the platform's share sheet to share text. - /// - /// Wraps the platform's native share dialog. Can share a text and/or a URL. - /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` - /// on iOS. - /// - /// The optional [subject] parameter can be used to populate a subject if the - /// user chooses to send an email. - /// - /// The optional [sharePositionOrigin] parameter can be used to specify a global - /// origin rect for the share sheet to popover from on iPads. It has no effect - /// on non-iPads. - /// - /// May throw [PlatformException] or [FormatException] - /// from [MethodChannel]. - static Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) { - assert(text != null); - assert(text.isNotEmpty); - final Map params = { - 'text': text, - 'subject': subject, - }; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - return channel.invokeMethod('share', params); - } - - /// Summons the platform's share sheet to share multiple files. - /// - /// Wraps the platform's native share dialog. Can share a file. - /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` - /// on iOS. - /// - /// The optional `sharePositionOrigin` parameter can be used to specify a global - /// origin rect for the share sheet to popover from on iPads. It has no effect - /// on non-iPads. - /// - /// May throw [PlatformException] or [FormatException] - /// from [MethodChannel]. - static Future shareFiles( - List paths, { - List? mimeTypes, - String? subject, - String? text, - Rect? sharePositionOrigin, - }) { - assert(paths != null); - assert(paths.isNotEmpty); - assert(paths.every((element) => element != null && element.isNotEmpty)); - final Map params = { - 'paths': paths, - 'mimeTypes': mimeTypes ?? - paths.map((String path) => _mimeTypeForPath(path)).toList(), - }; - - if (subject != null) params['subject'] = subject; - if (text != null) params['text'] = text; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - return channel.invokeMethod('shareFiles', params); - } - - static String _mimeTypeForPath(String path) { - assert(path != null); - return lookupMimeType(path) ?? 'application/octet-stream'; - } -} diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml deleted file mode 100644 index 4735995fff8a..000000000000 --- a/packages/share/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: share -description: Flutter plugin for sharing content via the platform share UI, using - the ACTION_SEND intent on Android and UIActivityViewController on iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/share -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+share%22 -version: 2.0.4 - -environment: - flutter: ">=1.12.13+hotfix.5" - sdk: ">=2.12.0 <3.0.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.share - pluginClass: SharePlugin - ios: - pluginClass: FLTSharePlugin - -dependencies: - meta: ^1.3.0 - mime: ^1.0.0 - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart deleted file mode 100644 index d0049cef94ab..000000000000 --- a/packages/share/test/share_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:share/share.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); // Required for MethodChannels - - late FakeMethodChannel fakeChannel; - - setUp(() { - fakeChannel = FakeMethodChannel(); - // Re-pipe to our fake to verify invocations. - Share.channel.setMockMethodCallHandler((MethodCall call) async { - // The explicit type can be void as the only method call has a return type of void. - await fakeChannel.invokeMethod(call.method, call.arguments); - }); - }); - - test('sharing empty fails', () { - expect( - () => Share.share(''), - throwsA(isA()), - ); - expect(fakeChannel.invocation, isNull); - }); - - test('sharing origin sets the right params', () async { - await Share.share( - 'some text to share', - subject: 'some subject to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - ); - - expect( - fakeChannel.invocation, - equals({ - 'share': { - 'text': 'some text to share', - 'subject': 'some subject to share', - 'originX': 1.0, - 'originY': 2.0, - 'originWidth': 3.0, - 'originHeight': 4.0, - } - }), - ); - }); - - test('sharing empty file fails', () { - expect( - () => Share.shareFiles(['']), - throwsA(isA()), - ); - expect(fakeChannel.invocation, isNull); - }); - - test('sharing file sets correct mimeType', () async { - final String path = 'tempfile-83649a.png'; - final File file = File(path); - try { - file.createSync(); - - await Share.shareFiles([path]); - - expect( - fakeChannel.invocation, - equals({ - 'shareFiles': { - 'paths': [path], - 'mimeTypes': ['image/png'], - } - }), - ); - } finally { - file.deleteSync(); - } - }); - - test('sharing file sets passed mimeType', () async { - final String path = 'tempfile-83649a.png'; - final File file = File(path); - try { - file.createSync(); - - await Share.shareFiles([path], mimeTypes: ['*/*']); - - expect( - fakeChannel.invocation, - equals({ - 'shareFiles': { - 'paths': [file.path], - 'mimeTypes': ['*/*'], - } - }), - ); - } finally { - file.deleteSync(); - } - }); -} - -class FakeMethodChannel extends Fake implements MethodChannel { - Map? invocation; - - @override - Future invokeMethod(String method, [dynamic arguments]) async { - this.invocation = {method: arguments}; - return null; - } -} diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index c172eeaf1223..4ec938bebce2 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: connectivity_for_web_integration_tests +name: video_player_for_web_integration_tests publish_to: none environment: diff --git a/packages/wifi_info_flutter/analysis_options.yaml b/packages/wifi_info_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/wifi_info_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/wifi_info_flutter/wifi_info_flutter/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/.metadata deleted file mode 100644 index e40242b8f94f..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: plugin diff --git a/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS b/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md deleted file mode 100644 index 0e41699be340..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -## NEXT - -* Updated Android lint settings. -* Updated package description. -* Updates Android compileSdkVersion to 31. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.4 - -* Android: Add Log warning for unsatisfied requirement(s) in Android P or higher. -* Android: Update Example project. - -## 1.0.3 - -* Fix README example. - -## 1.0.2 - -* Update Flutter SDK constraint. - -## 1.0.1 - -* Fixed method channel name in android implementation. [Issue](https://github.com/flutter/flutter/issues/69073). - -## 1.0.0 - -* Initial release of the plugin. This plugin retrieves information about a device's connection to wifi. -* See [README](./README.md) for details. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/LICENSE b/packages/wifi_info_flutter/wifi_info_flutter/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/README.md b/packages/wifi_info_flutter/wifi_info_flutter/README.md deleted file mode 100644 index 3f1084eb1d2c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# wifi_info_flutter - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`network_info_plus`](https://pub.dev/packages/network_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This plugin retrieves information about a device's connection to wifi. - -> Note that on Android, this does not guarantee connection to Internet. For instance, -the app might have wifi access but it might be a VPN or a hotel WiFi with no access. - -## Usage - -### Android - -Sample usage to check current status: - -To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, ensure all of the following conditions are met: - - * If your app is targeting Android 10 (API level 29) SDK or higher, your app has the ACCESS_FINE_LOCATION permission. - - * If your app is targeting SDK lower than Android 10 (API level 29), your app has the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission. - - * Location services are enabled on the device (under Settings > Location). - -You can get wi-fi related information using: - -```dart -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -var wifiBSSID = await WifiInfo().getWifiBSSID(); -var wifiIP = await WifiInfo().getWifiIP(); -var wifiName = await WifiInfo().getWifiName(); -``` - -### iOS 12 - -To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null. - -### iOS 13 - -The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. - -As of iOS 13, Apple announced that these APIs will no longer return valid information. -An app linked against iOS 12 or earlier receives pseudo-values such as: - - * SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU). - - * BSSID: "00:00:00:00:00:00" - -An app linked against iOS 13 or later receives `null`. - -The `CNCopyCurrentNetworkInfo` will work for Apps that: - - * The app uses Core Location, and has the user’s authorization to use location information. - - * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - - * The app has active VPN configurations installed. - -If your app falls into the last two categories, it will work as it is. If your app doesn't fall into the last two categories, -and you still need to access the wifi information, you should request user's authorization to use location information. - -There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`. -To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - -* `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. -* `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). - -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle deleted file mode 100644 index 76ac3bc7eedc..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -group 'io.flutter.plugins.wifi_info_flutter' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 31 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 01a286e96a21..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle deleted file mode 100644 index ec0e24958ea9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'wifi_info_flutter' diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index 03ac924f9427..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java deleted file mode 100644 index bd4c8f10ce3b..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java +++ /dev/null @@ -1,157 +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.wifi_info_flutter; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.LocationManager; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Build; -import androidx.core.content.ContextCompat; -import io.flutter.Log; - -/** Reports wifi information. */ -class WifiInfoFlutter { - private WifiManager wifiManager; - private Context context; - private static final String TAG = "WifiInfoFlutter"; - - WifiInfoFlutter(WifiManager wifiManager, Context context) { - this.wifiManager = wifiManager; - this.context = context; - } - - String getWifiName() { - if (!checkPermissions()) { - return null; - } - final WifiInfo wifiInfo = getWifiInfo(); - String ssid = null; - if (wifiInfo != null) ssid = wifiInfo.getSSID(); - if (ssid != null) ssid = ssid.replaceAll("\"", ""); // Android returns "SSID" - if (ssid != null && ssid.equals("")) ssid = null; - return ssid; - } - - String getWifiBSSID() { - if (!checkPermissions()) { - return null; - } - final WifiInfo wifiInfo = getWifiInfo(); - String bssid = null; - if (wifiInfo != null) { - bssid = wifiInfo.getBSSID(); - } - return bssid; - } - - String getWifiIPAddress() { - WifiInfo wifiInfo = null; - if (wifiManager != null) wifiInfo = wifiManager.getConnectionInfo(); - - String ip = null; - int i_ip = 0; - if (wifiInfo != null) i_ip = wifiInfo.getIpAddress(); - - if (i_ip != 0) - ip = - String.format( - "%d.%d.%d.%d", - (i_ip & 0xff), (i_ip >> 8 & 0xff), (i_ip >> 16 & 0xff), (i_ip >> 24 & 0xff)); - - return ip; - } - - private WifiInfo getWifiInfo() { - return wifiManager == null ? null : wifiManager.getConnectionInfo(); - } - - private Boolean checkPermissions() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return true; - } - - boolean grantedChangeWifiState = - ContextCompat.checkSelfPermission(context, Manifest.permission.CHANGE_WIFI_STATE) - == PackageManager.PERMISSION_GRANTED; - - boolean grantedAccessFine = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED; - - boolean grantedAccessCoarse = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && !grantedChangeWifiState - && !grantedAccessFine - && !grantedAccessCoarse) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, please ensure your app has one of the following permissions:\n" - + "- CHANGE_WIFI_STATE\n" - + "- ACCESS_FINE_LOCATION\n" - + "- ACCESS_COARSE_LOCATION\n" - + "For more information about Wi-Fi Restrictions in Android 8.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && !grantedChangeWifiState) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure your app has the CHANGE_WIFI_STATE permission.\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P - && !grantedAccessFine - && !grantedAccessCoarse) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, additional to CHANGE_WIFI_STATE please ensure your app has one of the following permissions too:\n" - + "- ACCESS_FINE_LOCATION\n" - + "- ACCESS_COARSE_LOCATION\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && (!grantedAccessFine || !grantedChangeWifiState)) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android Q, please ensure your app has the CHANGE_WIFI_STATE and ACCESS_FINE_LOCATION permission.\n" - + "For more information about Wi-Fi Restrictions in Android 10.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - LocationManager locationManager = - (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - - boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !gpsEnabled) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure Location services are enabled on the device (under Settings > Location).\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - return true; - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java deleted file mode 100644 index 9ceed5968e63..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java +++ /dev/null @@ -1,44 +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.wifi_info_flutter; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -/** - * The handler receives {@link MethodCall}s from the UIThread, gets the related information from - * a @{@link WifiInfoFlutter}, and then send the result back to the UIThread through the {@link - * MethodChannel.Result}. - */ -class WifiInfoFlutterMethodChannelHandler implements MethodChannel.MethodCallHandler { - private WifiInfoFlutter wifiInfoFlutter; - - /** - * Construct the WifiInfoFlutterMethodChannelHandler with a {@code wifiInfoFlutter}. The {@code - * wifiInfoFlutter} must not be null. - */ - WifiInfoFlutterMethodChannelHandler(WifiInfoFlutter wifiInfoFlutter) { - assert (wifiInfoFlutter != null); - this.wifiInfoFlutter = wifiInfoFlutter; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "wifiName": - result.success(wifiInfoFlutter.getWifiName()); - break; - case "wifiBSSID": - result.success(wifiInfoFlutter.getWifiBSSID()); - break; - case "wifiIPAddress": - result.success(wifiInfoFlutter.getWifiIPAddress()); - break; - default: - result.notImplemented(); - break; - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java deleted file mode 100644 index 7757688bc9fa..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java +++ /dev/null @@ -1,46 +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.wifi_info_flutter; - -import android.content.Context; -import android.net.wifi.WifiManager; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; - -/** WifiInfoFlutterPlugin */ -public class WifiInfoFlutterPlugin implements FlutterPlugin { - private MethodChannel methodChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - WifiInfoFlutterPlugin plugin = new WifiInfoFlutterPlugin(); - plugin.setupChannels(registrar.messenger(), registrar.context()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - private void setupChannels(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/wifi_info_flutter"); - final WifiManager wifiManager = - (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - - final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager, context); - - final WifiInfoFlutterMethodChannelHandler methodChannelHandler = - new WifiInfoFlutterMethodChannelHandler(wifiInfoFlutter); - methodChannel.setMethodCallHandler(methodChannelHandler); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata deleted file mode 100644 index 8407594c70b4..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: app diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/README.md b/packages/wifi_info_flutter/wifi_info_flutter/example/README.md deleted file mode 100644 index 30c38ad1ba92..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# wifi_info_flutter_example - -Demonstrates how to use the wifi_info_flutter plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle deleted file mode 100644 index 8f96b53e2663..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle +++ /dev/null @@ -1,54 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 31 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.flutter.plugins.wifi_info_flutter_example" - minSdkVersion 16 - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 357602a1d503..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index bcecab36d14a..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java deleted file mode 100644 index b52123be65d4..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java +++ /dev/null @@ -1,9 +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.wifi_info_flutter_example; - -import io.flutter.embedding.android.FlutterActivity; - -public class MainActivity extends FlutterActivity {} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index f74085f3f6a2..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 449a9f930826..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index d74aa35c2826..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 357602a1d503..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle deleted file mode 100644 index 456d020f6e2c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties deleted file mode 100644 index a6738207fd15..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true -android.enableR8=true diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 296b146b7318..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle deleted file mode 100644 index 44e62bcf06ae..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart deleted file mode 100644 index 8190062e3ebd..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('$WifiInfo test driver', () { - late WifiInfo _wifiInfo; - - setUpAll(() async { - _wifiInfo = WifiInfo(); - }); - - testWidgets('test location methods, iOS only', (WidgetTester tester) async { - expect( - (await _wifiInfo.getLocationServiceAuthorization()), - LocationAuthorizationStatus.notDetermined, - ); - }, skip: !Platform.isIOS); - }); -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index f2872cf474ee..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 9.0 - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile deleted file mode 100644 index 07a4e08abf54..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - # Work around https://github.com/flutter/flutter/issues/82964. - if target.name == 'Reachability' - target.build_configurations.each do |config| - config.build_settings['WARNING_CFLAGS'] = '-Wno-pointer-to-int-cast' - end - end - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 98b6089f0d68..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,545 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5304CE43F05781426D604828 /* libPods-Runner.a */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1E5A9EB282D46A445314F9FD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 2BADCFEAF6163E1D252C8765 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 5304CE43F05781426D604828 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 53534922C743E29B902DE7D2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5304CE43F05781426D604828 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 9938C0E7E0C974A660788A69 /* Pods */, - 53534922C743E29B902DE7D2 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 9938C0E7E0C974A660788A69 /* Pods */ = { - isa = PBXGroup; - children = ( - 1E5A9EB282D46A445314F9FD /* Pods-Runner.debug.xcconfig */, - 2BADCFEAF6163E1D252C8765 /* Pods-Runner.release.xcconfig */, - 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 77C985AE0A130001A78D36BE /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 77C985AE0A130001A78D36BE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6254f..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5ea15..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140cfdb3f..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5ea15..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 0681d288bb70..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h +++ /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. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 442514aaecbe..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "AppDelegate.h" -#import "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2d9d..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b70f1..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist deleted file mode 100644 index 2c7f6b8c6b85..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - wifi_info_flutter_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - NSLocationAlwaysAndWhenInUseUsageDescription - This app requires accessing your location information all the time to get wi-fi information. - NSLocationWhenInUseUsageDescription - This app requires accessing your location information when the app is in foreground to get wi-fi information. - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart deleted file mode 100644 index 8258815b0c09..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart +++ /dev/null @@ -1,173 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity/connectivity.dart' - show Connectivity, ConnectivityResult; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -// Sets a platform override for desktop to avoid exceptions. See -// https://flutter.dev/desktop#target-platform-override for more info. -void _enablePlatformOverrideForDesktop() { - if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - } -} - -void main() { - _enablePlatformOverrideForDesktop(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _connectionStatus = 'Unknown'; - final Connectivity _connectivity = Connectivity(); - final WifiInfo _wifiInfo = WifiInfo(); - late StreamSubscription _connectivitySubscription; - - @override - void initState() { - super.initState(); - initConnectivity(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); - } - - @override - void dispose() { - _connectivitySubscription.cancel(); - super.dispose(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - late ConnectivityResult result; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { - print(e.toString()); - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) { - return Future.value(null); - } - - return _updateConnectionStatus(result); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Connectivity example app'), - ), - body: Center(child: Text('Connection Status: $_connectionStatus')), - ); - } - - Future _updateConnectionStatus(ConnectivityResult result) async { - switch (result) { - case ConnectivityResult.wifi: - String? wifiName, wifiBSSID, wifiIP; - - try { - if (!kIsWeb && Platform.isIOS) { - LocationAuthorizationStatus status = - await _wifiInfo.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = await _wifiInfo.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _wifiInfo.getWifiName(); - } else { - wifiName = await _wifiInfo.getWifiName(); - } - } else { - wifiName = await _wifiInfo.getWifiName(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiName = "Failed to get Wifi Name"; - } - - try { - if (!kIsWeb && Platform.isIOS) { - LocationAuthorizationStatus status = - await _wifiInfo.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = await _wifiInfo.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } else { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } - } else { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiBSSID = "Failed to get Wifi BSSID"; - } - - try { - wifiIP = await _wifiInfo.getWifiIP(); - } on PlatformException catch (e) { - print(e.toString()); - wifiIP = "Failed to get Wifi IP"; - } - - setState(() { - _connectionStatus = '$result\n' - 'Wifi Name: $wifiName\n' - 'Wifi BSSID: $wifiBSSID\n' - 'Wifi IP: $wifiIP\n'; - }); - break; - case ConnectivityResult.mobile: - case ConnectivityResult.none: - setState(() => _connectionStatus = result.toString()); - break; - default: - setState(() => _connectionStatus = 'Failed to get connectivity.'); - break; - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml deleted file mode 100644 index 6303907c32d6..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: wifi_info_flutter_example -description: Demonstrates how to use the wifi_info_flutter plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - -dependencies: - connectivity: ^3.0.0 - flutter: - sdk: flutter - wifi_info_flutter: - # When depending on this package from a real application you should use: - # wifi_info_flutter: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart deleted file mode 100644 index 4f10f2a522f3..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep b/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h deleted file mode 100644 index 359562b7761c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@class FLTWifiInfoLocationDelegate; - -typedef void (^FLTWifiInfoLocationCompletion)(CLAuthorizationStatus); - -@interface FLTWifiInfoLocationHandler : NSObject - -+ (CLAuthorizationStatus)locationAuthorizationStatus; - -- (void)requestLocationAuthorization:(BOOL)always - completion:(_Nonnull FLTWifiInfoLocationCompletion)completionHnadler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m deleted file mode 100644 index 2fe19c5e70e9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTWifiInfoLocationHandler.h" - -@interface FLTWifiInfoLocationHandler () - -@property(copy, nonatomic) FLTWifiInfoLocationCompletion completion; -@property(strong, nonatomic) CLLocationManager *locationManager; - -@end - -@implementation FLTWifiInfoLocationHandler - -+ (CLAuthorizationStatus)locationAuthorizationStatus { - return CLLocationManager.authorizationStatus; -} - -- (void)requestLocationAuthorization:(BOOL)always - completion:(FLTWifiInfoLocationCompletion)completionHandler { - CLAuthorizationStatus status = CLLocationManager.authorizationStatus; - if (status != kCLAuthorizationStatusAuthorizedWhenInUse && always) { - completionHandler(kCLAuthorizationStatusDenied); - return; - } else if (status != kCLAuthorizationStatusNotDetermined) { - completionHandler(status); - return; - } - - if (self.completion) { - // If a request is still in process, immediately return. - completionHandler(kCLAuthorizationStatusNotDetermined); - return; - } - - self.completion = completionHandler; - self.locationManager = [CLLocationManager new]; - self.locationManager.delegate = self; - if (always) { - [self.locationManager requestAlwaysAuthorization]; - } else { - [self.locationManager requestWhenInUseAuthorization]; - } -} - -- (void)locationManager:(CLLocationManager *)manager - didChangeAuthorizationStatus:(CLAuthorizationStatus)status { - if (status == kCLAuthorizationStatusNotDetermined) { - return; - } - if (self.completion) { - self.completion(status); - self.completion = nil; - } -} - -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h deleted file mode 100644 index 41f165717809..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface WifiInfoFlutterPlugin : NSObject -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m deleted file mode 100644 index 47bd90c4429b..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "WifiInfoFlutterPlugin.h" - -#import -#import "FLTWifiInfoLocationHandler.h" -#import "SystemConfiguration/CaptiveNetwork.h" - -#include - -#include - -@interface WifiInfoFlutterPlugin () -@property(strong, nonatomic) FLTWifiInfoLocationHandler* locationHandler; -@end - -@implementation WifiInfoFlutterPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - WifiInfoFlutterPlugin* instance = [[WifiInfoFlutterPlugin alloc] init]; - - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/wifi_info_flutter" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (NSString*)findNetworkInfo:(NSString*)key { - NSString* info = nil; - NSArray* interfaceNames = (__bridge_transfer id)CNCopySupportedInterfaces(); - for (NSString* interfaceName in interfaceNames) { - NSDictionary* networkInfo = - (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName); - if (networkInfo[key]) { - info = networkInfo[key]; - } - } - return info; -} - -- (NSString*)getWifiName { - return [self findNetworkInfo:@"SSID"]; -} - -- (NSString*)getBSSID { - return [self findNetworkInfo:@"BSSID"]; -} - -- (NSString*)getWifiIP { - NSString* address = @"error"; - struct ifaddrs* interfaces = NULL; - struct ifaddrs* temp_addr = NULL; - int success = 0; - - // retrieve the current interfaces - returns 0 on success - success = getifaddrs(&interfaces); - if (success == 0) { - // Loop through linked list of interfaces - temp_addr = interfaces; - while (temp_addr != NULL) { - if (temp_addr->ifa_addr->sa_family == AF_INET) { - // Check if interface is en0 which is the wifi connection on the iPhone - if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { - // Get NSString from C String - address = [NSString - stringWithUTF8String:inet_ntoa(((struct sockaddr_in*)temp_addr->ifa_addr)->sin_addr)]; - } - } - - temp_addr = temp_addr->ifa_next; - } - } - - // Free memory - freeifaddrs(interfaces); - - return address; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"wifiName"]) { - result([self getWifiName]); - } else if ([call.method isEqualToString:@"wifiBSSID"]) { - result([self getBSSID]); - } else if ([call.method isEqualToString:@"wifiIPAddress"]) { - result([self getWifiIP]); - } else if ([call.method isEqualToString:@"getLocationServiceAuthorization"]) { - result([self convertCLAuthorizationStatusToString:[FLTWifiInfoLocationHandler - locationAuthorizationStatus]]); - } else if ([call.method isEqualToString:@"requestLocationServiceAuthorization"]) { - NSArray* arguments = call.arguments; - BOOL always = [arguments.firstObject boolValue]; - __weak typeof(self) weakSelf = self; - [self.locationHandler - requestLocationAuthorization:always - completion:^(CLAuthorizationStatus status) { - result([weakSelf convertCLAuthorizationStatusToString:status]); - }]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (NSString*)convertCLAuthorizationStatusToString:(CLAuthorizationStatus)status { - switch (status) { - case kCLAuthorizationStatusNotDetermined: { - return @"notDetermined"; - } - case kCLAuthorizationStatusRestricted: { - return @"restricted"; - } - case kCLAuthorizationStatusDenied: { - return @"denied"; - } - case kCLAuthorizationStatusAuthorizedAlways: { - return @"authorizedAlways"; - } - case kCLAuthorizationStatusAuthorizedWhenInUse: { - return @"authorizedWhenInUse"; - } - default: { - return @"unknown"; - } - } -} - -- (FLTWifiInfoLocationHandler*)locationHandler { - if (!_locationHandler) { - _locationHandler = [FLTWifiInfoLocationHandler new]; - } - return _locationHandler; -} -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec b/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec deleted file mode 100644 index c3b3416ad767..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint wifi_info_flutter.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'wifi_info_flutter' - s.version = '0.0.1' - s.summary = 'A wifi information plugin for Flutter.' - s.description = <<-DESC -A Flutter plugin for retrieving wifi information from a device. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master' } - s.documentation_url = 'https://pub.dev' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } -end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart deleted file mode 100644 index 1c89ee82fc3e..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; - -// Export enums from the platform_interface so plugin users can use them directly. -export 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart' - show LocationAuthorizationStatus; - -/// Checks WI-FI status and more. -class WifiInfo { - WifiInfo._(); - - /// Constructs a singleton instance of [WifiInfo]. - /// - /// [WifiInfo] is designed to work as a singleton. - factory WifiInfo() => _singleton; - - static final WifiInfo _singleton = WifiInfo._(); - - static WifiInfoFlutterPlatform get _platform => - WifiInfoFlutterPlatform.instance; - - /// Obtains the wifi name (SSID) of the connected network - /// - /// Please note that it DOESN'T WORK on emulators (returns null). - /// - /// From android 8.0 onwards the GPS must be ON (high accuracy) - /// in order to be able to obtain the SSID. - Future getWifiName() { - return _platform.getWifiName(); - } - - /// Obtains the wifi BSSID of the connected network. - /// - /// Please note that it DOESN'T WORK on emulators (returns null). - /// - /// From Android 8.0 onwards the GPS must be ON (high accuracy) - /// in order to be able to obtain the BSSID. - Future getWifiBSSID() { - return _platform.getWifiBSSID(); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - return _platform.getWifiIP(); - } - - /// Request to authorize the location service (Only on iOS). - /// - /// This method will throw a [PlatformException] on Android. - /// - /// Returns a [LocationAuthorizationStatus] after user authorized or denied the location on this request. - /// - /// If the location information needs to be accessible all the time, set `requestAlwaysLocationUsage` to true. If user has - /// already granted a [LocationAuthorizationStatus.authorizedWhenInUse] prior to requesting an "always" access, it will return [LocationAuthorizationStatus.denied]. - /// - /// If the location service authorization is not determined prior to making this call, a platform standard UI of requesting a location service will pop up. - /// This UI will only show once unless the user re-install the app to their phone which resets the location service authorization to not determined. - /// - /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. - /// It can be replaced with other permission handling code/plugin if preferred. - /// To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - /// * `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information - /// all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. - /// * `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is - /// running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. - /// - /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: - /// - /// * The app uses Core Location, and has the user’s authorization to use location information. - /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - /// * The app has active VPN configurations installed. - /// - /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. - /// For example, - /// ```dart - /// if (Platform.isIOS) { - /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); - /// if (status == LocationAuthorizationStatus.notDetermined) { - /// status = await _connectivity.requestLocationServiceAuthorization(); - /// } - /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } else { - /// print('location service is not authorized, the data might not be correct'); - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// } else { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// ``` - /// - /// Ideally, a location service authorization should only be requested if the current authorization status is not determined. - /// - /// See also [getLocationServiceAuthorization] to obtain current location service status. - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - return _platform.requestLocationServiceAuthorization( - requestAlwaysLocationUsage: requestAlwaysLocationUsage, - ); - } - - /// Get the current location service authorization (Only on iOS). - /// - /// This method will throw a [PlatformException] on Android. - /// - /// Returns a [LocationAuthorizationStatus]. - /// If the returned value is [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] call - /// can request the authorization. - /// If the returned value is not [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] - /// will not initiate another request. It will instead return the "determined" status. - /// - /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. - /// It can be replaced with other permission handling code/plugin if preferred. - /// - /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: - /// - /// * The app uses Core Location, and has the user’s authorization to use location information. - /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - /// * The app has active VPN configurations installed. - /// - /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. - /// For example, - /// ```dart - /// if (Platform.isIOS) { - /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); - /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } else { - /// print('location service is not authorized, the data might not be correct'); - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// } else { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// ``` - /// - /// See also [requestLocationServiceAuthorization] for requesting a location service authorization. - Future getLocationServiceAuthorization() { - return _platform.getLocationServiceAuthorization(); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml deleted file mode 100644 index b1e1e756c633..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: wifi_info_flutter -description: A Flutter plugin to get WiFi information such as connection status and network identifiers. -repository: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+wifi_info_flutter%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.wifi_info_flutter - pluginClass: WifiInfoFlutterPlugin - ios: - pluginClass: WifiInfoFlutterPlugin - -dependencies: - flutter: - sdk: flutter - wifi_info_flutter_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart deleted file mode 100644 index 93cf378de437..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; -import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; - -const String kWifiNameResult = '1337wifi'; -const String kWifiBSSIDResult = 'c0:ff:33:c0:d3:55'; -const String kWifiIpAddressResult = '127.0.0.1'; -const LocationAuthorizationStatus kRequestLocationResult = - LocationAuthorizationStatus.authorizedAlways; -const LocationAuthorizationStatus kGetLocationResult = - LocationAuthorizationStatus.authorizedAlways; - -void main() { - group('$WifiInfo', () { - late WifiInfo wifiInfo; - MockWifiInfoFlutterPlatform fakePlatform; - - setUp(() async { - fakePlatform = MockWifiInfoFlutterPlatform(); - WifiInfoFlutterPlatform.instance = fakePlatform; - wifiInfo = WifiInfo(); - }); - - test('getWifiName', () async { - String? result = await wifiInfo.getWifiName(); - expect(result, kWifiNameResult); - }); - - test('getWifiBSSID', () async { - String? result = await wifiInfo.getWifiBSSID(); - expect(result, kWifiBSSIDResult); - }); - - test('getWifiIP', () async { - String? result = await wifiInfo.getWifiIP(); - expect(result, kWifiIpAddressResult); - }); - - test('requestLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await wifiInfo.requestLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); - - test('getLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await wifiInfo.getLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); - }); -} - -class MockWifiInfoFlutterPlatform extends WifiInfoFlutterPlatform { - @override - Future getWifiName() async { - return kWifiNameResult; - } - - @override - Future getWifiBSSID() async { - return kWifiBSSIDResult; - } - - @override - Future getWifiIP() async { - return kWifiIpAddressResult; - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) async { - return kRequestLocationResult; - } - - @override - Future getLocationServiceAuthorization() async { - return kGetLocationResult; - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata deleted file mode 100644 index a6d65e2f3343..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: package diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the Flutter project. Names should be added to the list like so: -# -# Name/Organization - -Google Inc. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md deleted file mode 100644 index 34f8e84cd780..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.1 - -* Update Flutter SDK constraint. - -## 1.0.0 - -* Initial release of package. Includes support for retrieving wifi name, wifi BSSID, wifi ip address -and requesting location service authorization. diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md deleted file mode 100644 index f2039c3d5865..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# wifi_info_flutter_platform_interface - -A common platform interface for the [`wifi_info_flutter`][1] plugin. - -This interface allows platform-specific implementations of the `wifi_info_flutter` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `wifi_info_flutter`, extend -[`WifiInfoFlutterPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`WifiInfoFlutterPlatform` by calling -`WifiInfoFlutterPlatform.instance = MyPlatformWifiInfoFlutter()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../ -[2]: lib/wifi_info_flutter_platform_interface.dart diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart deleted file mode 100644 index d5f05e6121a9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart +++ /dev/null @@ -1,24 +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. - -/// The status of the location service authorization. -enum LocationAuthorizationStatus { - /// The authorization of the location service is not determined. - notDetermined, - - /// This app is not authorized to use location. - restricted, - - /// User explicitly denied the location service. - denied, - - /// User authorized the app to access the location at any time. - authorizedAlways, - - /// User authorized the app to access the location when the app is visible to them. - authorizedWhenInUse, - - /// Status unknown. - unknown -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart deleted file mode 100644 index 79f27e8cde44..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import '../wifi_info_flutter_platform_interface.dart'; - -/// An implementation of [WifiInfoFlutterPlatform] that uses method channels. -class MethodChannelWifiInfoFlutter extends WifiInfoFlutterPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - MethodChannel('plugins.flutter.io/wifi_info_flutter'); - - @override - Future getWifiName() async { - return methodChannel.invokeMethod('wifiName'); - } - - @override - Future getWifiBSSID() { - return methodChannel.invokeMethod('wifiBSSID'); - } - - @override - Future getWifiIP() { - return methodChannel.invokeMethod('wifiIPAddress'); - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - return methodChannel.invokeMethod( - 'requestLocationServiceAuthorization', [ - requestAlwaysLocationUsage - ]).then(_parseLocationAuthorizationStatus); - } - - @override - Future getLocationServiceAuthorization() { - return methodChannel - .invokeMethod('getLocationServiceAuthorization') - .then(_parseLocationAuthorizationStatus); - } -} - -/// Convert a String to a LocationAuthorizationStatus value. -LocationAuthorizationStatus _parseLocationAuthorizationStatus(String? result) { - return LocationAuthorizationStatus.values.firstWhere( - (LocationAuthorizationStatus status) => result == describeEnum(status), - orElse: () => LocationAuthorizationStatus.unknown, - ); -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart deleted file mode 100644 index 62330d4261a0..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'src/enums.dart'; -import 'src/method_channel_wifi_info_flutter.dart'; - -export 'src/enums.dart'; - -/// The interface that implementations of wifi_info_flutter must implement. -/// -/// Platform implementations should extend this class rather than implement it -/// as `wifi_info_flutter` does not consider newly added methods to be breaking -/// changes. Extending this class (using `extends`) ensures that the subclass -/// will get the default implementation, while platform implementations that -/// `implements` this interface will be broken by newly added -/// [ConnectivityPlatform] methods. -abstract class WifiInfoFlutterPlatform extends PlatformInterface { - /// Constructs a WifiInfoFlutterPlatform. - WifiInfoFlutterPlatform() : super(token: _token); - - static final Object _token = Object(); - - static WifiInfoFlutterPlatform _instance = MethodChannelWifiInfoFlutter(); - - /// The default instance of [WifiInfoFlutterPlatform] to use. - /// - /// Defaults to [MethodChannelWifiInfoFlutter]. - static WifiInfoFlutterPlatform get instance => _instance; - - /// Set the default instance of [WifiInfoFlutterPlatform] to use. - /// - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [WifiInfoFlutterPlatform] when they register - /// themselves. - static set instance(WifiInfoFlutterPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { - throw UnimplementedError('getWifiName() has not been implemented.'); - } - - /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { - throw UnimplementedError('getWifiBSSID() has not been implemented.'); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - throw UnimplementedError('getWifiIP() has not been implemented.'); - } - - /// Request to authorize the location service (Only on iOS). - Future requestLocationServiceAuthorization( - {bool requestAlwaysLocationUsage = false}) { - throw UnimplementedError( - 'requestLocationServiceAuthorization() has not been implemented.', - ); - } - - /// Get the current location service authorization (Only on iOS). - Future getLocationServiceAuthorization() { - throw UnimplementedError( - 'getLocationServiceAuthorization() has not been implemented.', - ); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml deleted file mode 100644 index 14ca643aa045..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: wifi_info_flutter_platform_interface -description: A common platform interface for the wifi_info_flutter plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+wifi_info_flutter%22 -version: 2.0.1 -# NOTE: We strongly prefer non-breaking changes, even at the expense of a -# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" - -dependencies: - plugin_platform_interface: ^2.0.0 - flutter: - sdk: flutter - -dev_dependencies: - pedantic: ^1.10.0 - flutter_test: - sdk: flutter diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart deleted file mode 100644 index 875f2ab4089a..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:wifi_info_flutter_platform_interface/src/enums.dart'; -import 'package:wifi_info_flutter_platform_interface/src/method_channel_wifi_info_flutter.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelWifiInfoFlutter', () { - final List log = []; - late MethodChannelWifiInfoFlutter methodChannelWifiInfoFlutter; - - setUp(() async { - methodChannelWifiInfoFlutter = MethodChannelWifiInfoFlutter(); - - methodChannelWifiInfoFlutter.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'wifiName': - return '1337wifi'; - case 'wifiBSSID': - return 'c0:ff:33:c0:d3:55'; - case 'wifiIPAddress': - return '127.0.0.1'; - case 'requestLocationServiceAuthorization': - return 'authorizedAlways'; - case 'getLocationServiceAuthorization': - return 'authorizedAlways'; - default: - return null; - } - }); - log.clear(); - }); - - test('getWifiName', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiName(); - expect(result, '1337wifi'); - expect( - log, - [ - isMethodCall( - 'wifiName', - arguments: null, - ), - ], - ); - }); - - test('getWifiBSSID', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiBSSID(); - expect(result, 'c0:ff:33:c0:d3:55'); - expect( - log, - [ - isMethodCall( - 'wifiBSSID', - arguments: null, - ), - ], - ); - }); - - test('getWifiIP', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiIP(); - expect(result, '127.0.0.1'); - expect( - log, - [ - isMethodCall( - 'wifiIPAddress', - arguments: null, - ), - ], - ); - }); - - test('requestLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelWifiInfoFlutter - .requestLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'requestLocationServiceAuthorization', - arguments: [false], - ), - ], - ); - }); - - test('getLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelWifiInfoFlutter.getLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'getLocationServiceAuthorization', - arguments: null, - ), - ], - ); - }); - }); -} diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index c1c4a8d3f09c..81bb0f5db59c 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -23,16 +23,3 @@ - ios_platform_images - local_auth - video_player - -# These plugins are deprecated in favor of the Community Plus versions, and -# will be removed from the repo once the critical support window has passed, -# so are not worth updating. -- android_alarm_manager -- android_intent -- battery -- connectivity -- device_info -- package_info -- sensors -- share -- wifi_info_flutter diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml index e06b062fd68f..653f3516727b 100644 --- a/script/configs/exclude_integration_android.yaml +++ b/script/configs/exclude_integration_android.yaml @@ -1,10 +1,2 @@ -# Deprecated; no plan to backfill the missing files -- android_intent -- connectivity/connectivity -- device_info/device_info -- sensors -- share -- wifi_info_flutter/wifi_info_flutter - # No integration tests to run: - espresso diff --git a/script/configs/exclude_integration_ios.yaml b/script/configs/exclude_integration_ios.yaml index a30dffbafde6..06283ae59c16 100644 --- a/script/configs/exclude_integration_ios.yaml +++ b/script/configs/exclude_integration_ios.yaml @@ -2,5 +2,3 @@ - in_app_purchase_storekit # Currently missing: https://github.com/flutter/flutter/issues/82208 - ios_platform_images -# Hangs on CI. Deprecated, so there is no plan to fix it. -- sensors diff --git a/script/configs/exclude_native_ios.yaml b/script/configs/exclude_native_ios.yaml deleted file mode 100644 index 723fcfa64715..000000000000 --- a/script/configs/exclude_native_ios.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Deprecated; no plan to backfill the missing files -- battery -- connectivity/connectivity -- device_info/device_info -- package_info -- sensors -- wifi_info_flutter/wifi_info_flutter diff --git a/script/configs/exclude_native_macos.yaml b/script/configs/exclude_native_macos.yaml deleted file mode 100644 index 8a817a9c0178..000000000000 --- a/script/configs/exclude_native_macos.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Deprecated plugins that will not be getting unit test backfill. -- connectivity_macos -- package_info diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml index 5ec80eee73a0..45197b94962f 100644 --- a/script/configs/exclude_native_unit_android.yaml +++ b/script/configs/exclude_native_unit_android.yaml @@ -1,11 +1,2 @@ -# Deprecated; no plan to backfill the missing files -- android_alarm_manager -- battery -- device_info/device_info -- package_info -- sensors -- share -- wifi_info_flutter/wifi_info_flutter - # No need for unit tests: - espresso diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 234700ab5a7d..dec218e2ba24 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,9 @@ dependencies to path-based dependencies. - Adds a (hidden) `--run-on-dirty-packages` flag for use with `make-deps-path-based` in CI. +- Fix `federation-safety-check` handling of plugin deletion, and of top-level + files in unfederated plugins whose names match federated plugin heuristics + (e.g., `packages/foo/foo_android.iml`). ## 0.7.3 diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 200f9c3f48cb..df9d86892e13 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -81,7 +81,8 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { // Count the top-level plugin as changed. _changedPlugins.add(packageName); if (relativeComponents[0] == packageName || - relativeComponents[0].startsWith('${packageName}_')) { + (relativeComponents.length > 1 && + relativeComponents[0].startsWith('${packageName}_'))) { packageName = relativeComponents.removeAt(0); } @@ -178,6 +179,10 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { String pubspecRepoRelativePosixPath) async { final File pubspecFile = childFileWithSubcomponents( packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + if (!pubspecFile.existsSync()) { + // If the package was deleted, nothing will be published. + return false; + } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo == 'none') { return false; 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 04869639cf74..5ee42848a81b 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -196,19 +196,27 @@ dependency_overrides: Future> _getNonBreakingUpdatePackages() async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final String baseSha = await gitVersionFinder.getBaseSha(); - print('Finding changed packages relative to "$baseSha"\n'); + print('Finding changed packages relative to "$baseSha"...'); final Set changedPackages = {}; - for (final String path in await gitVersionFinder.getChangedFiles()) { + for (final String changedPath in await gitVersionFinder.getChangedFiles()) { // Git output always uses Posix paths. - final List allComponents = p.posix.split(path); + final List allComponents = p.posix.split(changedPath); // Only pubspec changes are potential publishing events. if (allComponents.last != 'pubspec.yaml' || allComponents.contains('example')) { continue; } final RepositoryPackage package = - RepositoryPackage(packagesDir.fileSystem.file(path).parent); + RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); + // Ignored deleted packages, as they won't be published. + if (!package.pubspecFile.existsSync()) { + final String directoryName = p.posix.joinAll(path.split(path.relative( + package.directory.absolute.path, + from: packagesDir.path))); + print(' Skipping $directoryName; deleted.'); + continue; + } final String packageName = package.parsePubspec().name; if (!await _hasNonBreakingVersionChange(package)) { // Log packages that had pubspec changes but weren't included for ease diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart index e23485fbc8b7..126aa8b41c83 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -352,4 +352,64 @@ void main() { ]), ); }); + + test('handles top-level files that match federated package heuristics', + () async { + final Directory plugin = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + // This should be picked up as a change to 'foo', and not crash. + plugin.childFile('foo_bar.baz'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + ]), + ); + }); + + test('handles deletion of an entire plugin', () async { + // Simulate deletion, in the form of diffs for packages that don't exist in + // the filesystem. + final String changedFileOutput = [ + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir + .childDirectory('foo_platform_interface') + .childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_platform_interface') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir.childDirectory('foo_web').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_web') + .childDirectory('lib') + .childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Ran for 0 package(s)'), + ]), + ); + }); } 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 29e4f724b338..bdd2139b237c 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -173,6 +173,29 @@ void main() { ); }); + test('no-ops for no deleted packages', () async { + final String changedFileOutput = [ + // A change for a file that's not on disk simulates a deletion. + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Skipping foo; deleted.'), + contains('No target dependencies'), + ]), + ); + }); + test('includes bugfix version changes as targets', () async { const String newVersion = '1.0.1'; final Directory package = From 5012b95815775ef14a4e0d160c77fc50111bc1d1 Mon Sep 17 00:00:00 2001 From: e-adrien Date: Mon, 6 Dec 2021 20:12:30 +0000 Subject: [PATCH 220/259] [webview_flutter] Add a backgroundColor option to the Android webview (#4569) This PR add an option to set the background color of the Android webview. Part of: https://github.com/flutter/plugins/pull/3431 Part of: https://github.com/flutter/flutter/issues/29300 --- .../webview_flutter_android/CHANGELOG.md | 11 ++-- .../GeneratedAndroidWebView.java | 33 ++++++++++ .../webviewflutter/WebViewHostApiImpl.java | 6 ++ .../BackgroundColorTest.java | 61 +++++++++++++++++++ .../android/app/src/debug/AndroidManifest.xml | 8 +++ .../DriverExtensionActivity.java | 16 +++++ .../example/lib/main.dart | 44 +++++++++++++ .../example/lib/web_view.dart | 8 +++ .../lib/src/android_webview.dart | 7 +++ .../lib/src/android_webview.pigeon.dart | 25 ++++++++ .../lib/src/android_webview_api_impls.dart | 5 ++ .../lib/webview_android_widget.dart | 5 ++ .../pigeons/android_webview.dart | 2 + .../webview_flutter_android/pubspec.yaml | 5 +- .../test/android_webview.pigeon.dart | 25 +++++++- .../test/android_webview_test.mocks.dart | 10 +++ .../webview_android_widget_test.mocks.dart | 18 ++++-- 17 files changed, 275 insertions(+), 14 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 479364ff14a7..caee5f7cb75a 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.5.0 + +* Adds an option to set the background color of the webview. + ## 2.4.0 * Adds support for Android's `WebView.loadData` and `WebView.loadDataWithBaseUrl` methods and implements the `loadFile` and `loadHtmlString` methods from the platform interface. @@ -27,13 +31,12 @@ when it is created without Hybrid Composition. ## 2.0.15 -* Added Overrides in FlutterWebView.java - +* Added Overrides in FlutterWebView.java + ## 2.0.14 -* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). ## 2.0.13 * Extract Android implementation from `webview_flutter`. - diff --git a/packages/webview_flutter/webview_flutter_android/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 beb2d7105f22..a5632d351f83 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 @@ -223,6 +223,8 @@ void loadDataWithBaseUrl( void setWebChromeClient(Long instanceId, Long clientInstanceId); + void setBackgroundColor(Long instanceId, Long color); + /** The codec used by WebViewHostApi. */ static MessageCodec getCodec() { return WebViewHostApiCodec.INSTANCE; @@ -958,6 +960,37 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewHostApi.setBackgroundColor", + 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."); + } + Number colorArg = (Number) args.get(1); + if (colorArg == null) { + throw new NullPointerException("colorArg unexpectedly null."); + } + api.setBackgroundColor(instanceIdArg.longValue(), colorArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } 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 b557e9889704..78b06aac90b1 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 @@ -502,6 +502,12 @@ public void setWebChromeClient(Long instanceId, Long clientInstanceId) { webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId)); } + @Override + 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)) { diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java new file mode 100644 index 000000000000..a63629e0b9e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static org.junit.Assert.assertEquals; + +import android.graphics.Bitmap; +import android.graphics.Color; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.screenshot.ScreenCapture; +import androidx.test.runner.screenshot.Screenshot; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class BackgroundColorTest { + @Rule + public ActivityTestRule myActivityTestRule = + new ActivityTestRule<>(DriverExtensionActivity.class, true, false); + + @Before + public void setUp() { + ActivityScenario.launch(DriverExtensionActivity.class); + } + + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + @Test + public void backgroundColor() { + onFlutterWidget(withValueKey("ShowPopupMenu")).perform(click()); + onFlutterWidget(withValueKey("ShowTransparentBackgroundExample")).perform(click()); + onFlutterWidget(withText("Transparent background test")); + + final ScreenCapture screenCapture = Screenshot.capture(); + final Bitmap screenBitmap = screenCapture.getBitmap(); + + final int centerLeftColor = + screenBitmap.getPixel(10, (int) Math.floor(screenBitmap.getHeight() / 2.0)); + final int centerColor = + screenBitmap.getPixel( + (int) Math.floor(screenBitmap.getWidth() / 2.0), + (int) Math.floor(screenBitmap.getHeight() / 2.0)); + + // Flutter Colors.green color : 0xFF4CAF50 + // https://github.com/flutter/flutter/blob/f4abaa0735eba4dfd8f33f73363911d63931fe03/packages/flutter/lib/src/material/colors.dart#L1208 + // The background color of the webview is : rgba(0, 0, 0, 0.5) + // The expected color is : rgba(38, 87, 40, 1) -> 0xFF265728 + assertEquals(0xFF265728, centerLeftColor); + assertEquals(Color.RED, centerColor); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml index 28792201bc36..110b9abe1cd8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -13,5 +13,13 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java new file mode 100644 index 000000000000..59e1b04c37f2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; + +public class DriverExtensionActivity extends FlutterActivity { + @Override + @NonNull + public String getDartEntrypointFunctionName() { + return "appMain"; + } +} 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 bc43c16d4498..0c04c8ca4004 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_android/webview_surface_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; @@ -17,6 +18,11 @@ import 'navigation_decision.dart'; import 'navigation_request.dart'; import 'web_view.dart'; +void appMain() { + enableFlutterDriverExtension(); + main(); +} + void main() { // Configure the [WebView] to use the [SurfaceAndroidWebView] // implementation instead of the default [AndroidWebView]. @@ -59,6 +65,27 @@ const String kExamplePage = ''' '''; +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + class _WebViewExample extends StatefulWidget { const _WebViewExample({Key? key}) : super(key: key); @@ -73,6 +100,7 @@ class _WebViewExampleState extends State<_WebViewExample> { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF4CAF50), appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -109,6 +137,7 @@ class _WebViewExampleState extends State<_WebViewExample> { javascriptChannels: _createJavascriptChannels(context), javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', + backgroundColor: const Color(0x80000000), ); }), floatingActionButton: favoriteButton(), @@ -158,6 +187,7 @@ enum _MenuOptions { navigationDelegate, loadLocalFile, loadHtmlString, + transparentBackground, } class _SampleMenu extends StatelessWidget { @@ -172,6 +202,7 @@ class _SampleMenu extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot controller) { return PopupMenuButton<_MenuOptions>( + key: const ValueKey('ShowPopupMenu'), onSelected: (_MenuOptions value) { switch (value) { case _MenuOptions.showUserAgent: @@ -201,6 +232,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.loadHtmlString: _onLoadHtmlStringExample(controller.data!, context); break; + case _MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -241,6 +275,11 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.loadLocalFile, child: Text('Load local file'), ), + const PopupMenuItem<_MenuOptions>( + key: ValueKey('ShowTransparentBackgroundExample'), + value: _MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), ], ); }, @@ -353,6 +392,11 @@ class _SampleMenu extends StatelessWidget { return indexFile.path; } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } } class _NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 654abbd960cd..395966bdd744 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -76,6 +76,7 @@ class WebView extends StatefulWidget { this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -236,6 +237,12 @@ class WebView extends StatefulWidget { /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override _WebViewState createState() => _WebViewState(); } @@ -287,6 +294,7 @@ class _WebViewState extends State { _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + backgroundColor: widget.backgroundColor, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); 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 928cdb3099f1..ecd6f33d1e63 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 @@ -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:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' show AndroidViewSurface; @@ -349,6 +351,11 @@ class WebView { return api.setWebChromeClientFromInstance(this, client); } + /// Sets the background color of this WebView. + Future setBackgroundColor(Color color) { + return api.setBackgroundColorFromInstance(this, color.value); + } + /// Releases all resources used by the [WebView]. /// /// Any methods called after [release] will throw an exception. 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 9bf2de6d169b..ae528a64bb8f 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 @@ -708,6 +708,31 @@ class WebViewHostApi { return; } } + + Future setBackgroundColor(int arg_instanceId, int arg_color) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } } class _WebSettingsHostApiCodec extends StandardMessageCodec { 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 ece05b56c182..51efdaadebdc 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 @@ -278,6 +278,11 @@ class WebViewHostApiImpl extends WebViewHostApi { instanceManager.getInstanceId(client)!, ); } + + /// Helper method to convert instances ids to objects. + Future setBackgroundColorFromInstance(WebView instance, int color) { + return setBackgroundColor(instanceManager.getInstanceId(instance)!, color); + } } /// Host api implementation for [WebSettings]. 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 c264bc1b9e6e..0bfa04fde095 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 @@ -298,6 +298,11 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { AutoMediaPlaybackPolicy.always_allow, ); + final Color? backgroundColor = creationParams.backgroundColor; + if (backgroundColor != null) { + webView.setBackgroundColor(backgroundColor); + } + addJavascriptChannels(creationParams.javascriptChannelNames); } 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 963216329237..0fdec2c17756 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -87,6 +87,8 @@ abstract class WebViewHostApi { void setDownloadListener(int instanceId, int listenerInstanceId); void setWebChromeClient(int instanceId, int clientInstanceId); + + void setBackgroundColor(int instanceId, int color); } @HostApi(dartHostTestHandler: 'TestWebSettingsHostApi') diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 8889469920df..75245a31e4c2 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.4.0 +version: 2.5.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.5.2 + webview_flutter_platform_interface: ^1.7.0 dev_dependencies: build_runner: ^2.1.4 @@ -30,4 +30,3 @@ dev_dependencies: mockito: ^5.0.16 pedantic: ^1.10.0 pigeon: 1.0.9 - diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 45988a0ae441..942e59a0b2b3 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -4,7 +4,7 @@ // Autogenerated from Pigeon (v1.0.9), 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, avoid_relative_lib_imports, unnecessary_parenthesis +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import // @dart = 2.12 import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; @@ -46,6 +46,7 @@ abstract class TestWebViewHostApi { void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); 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}) { { @@ -577,6 +578,28 @@ abstract class TestWebViewHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); + final int? arg_color = (args[1] as int?); + assert(arg_color != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); + api.setBackgroundColor(arg_instanceId!, arg_color!); + return {}; + }); + } + } } } 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 90cbf2c4fefe..3c1cd610c136 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 @@ -3,6 +3,7 @@ // Do not manually edit this file. import 'dart:async' as _i4; +import 'dart:ui' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; @@ -332,6 +333,10 @@ class MockTestWebViewHostApi extends _i1.Mock #setWebChromeClient, [instanceId, clientInstanceId]), returnValueForMissingStub: null); @override + void setBackgroundColor(int? instanceId, int? color) => super.noSuchMethod( + Invocation.method(#setBackgroundColor, [instanceId, color]), + returnValueForMissingStub: null); + @override String toString() => super.toString(); } @@ -485,6 +490,11 @@ class MockWebView extends _i1.Mock implements _i2.WebView { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override + _i4.Future setBackgroundColor(_i5.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override _i4.Future release() => (super.noSuchMethod(Invocation.method(#release, []), returnValue: Future.value(), 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 0582049acf6d..6ee53f9f9362 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 @@ -3,10 +3,11 @@ // Do not manually edit this file. import 'dart:async' as _i4; +import 'dart:ui' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; -import 'package:webview_flutter_android/webview_android_widget.dart' as _i5; +import 'package:webview_flutter_android/webview_android_widget.dart' as _i6; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' as _i3; @@ -229,6 +230,11 @@ class MockWebView extends _i1.Mock implements _i2.WebView { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override + _i4.Future setBackgroundColor(_i5.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override _i4.Future release() => (super.noSuchMethod(Invocation.method(#release, []), returnValue: Future.value(), @@ -241,7 +247,7 @@ class MockWebView extends _i1.Mock implements _i2.WebView { /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidDownloadListener extends _i1.Mock - implements _i5.WebViewAndroidDownloadListener { + implements _i6.WebViewAndroidDownloadListener { MockWebViewAndroidDownloadListener() { _i1.throwOnMissingStub(this); } @@ -267,7 +273,7 @@ class MockWebViewAndroidDownloadListener extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidJavaScriptChannel extends _i1.Mock - implements _i5.WebViewAndroidJavaScriptChannel { + implements _i6.WebViewAndroidJavaScriptChannel { MockWebViewAndroidJavaScriptChannel() { _i1.throwOnMissingStub(this); } @@ -293,7 +299,7 @@ class MockWebViewAndroidJavaScriptChannel extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidWebChromeClient extends _i1.Mock - implements _i5.WebViewAndroidWebChromeClient { + implements _i6.WebViewAndroidWebChromeClient { MockWebViewAndroidWebChromeClient() { _i1.throwOnMissingStub(this); } @@ -310,7 +316,7 @@ class MockWebViewAndroidWebChromeClient extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidWebViewClient extends _i1.Mock - implements _i5.WebViewAndroidWebViewClient { + implements _i6.WebViewAndroidWebViewClient { MockWebViewAndroidWebViewClient() { _i1.throwOnMissingStub(this); } @@ -442,7 +448,7 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock /// A class which mocks [WebViewProxy]. /// /// See the documentation for Mockito's code generation for more information. -class MockWebViewProxy extends _i1.Mock implements _i5.WebViewProxy { +class MockWebViewProxy extends _i1.Mock implements _i6.WebViewProxy { MockWebViewProxy() { _i1.throwOnMissingStub(this); } From 7d9cc848dfd356ecc8c456b1d451d8c92a0fa507 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 6 Dec 2021 23:34:05 +0100 Subject: [PATCH 221/259] [webview_flutter] Expose loadFile and loadHtmlString through app facing package. (#4558) --- .../webview_flutter/CHANGELOG.md | 3 +- .../webview_flutter/example/lib/main.dart | 59 ++++++++++++ .../webview_flutter/example/pubspec.yaml | 1 + .../webview_flutter/lib/src/webview.dart | 29 ++++++ .../webview_flutter/pubspec.yaml | 4 +- .../test/webview_flutter_test.dart | 92 +++++++++++++++++++ .../test/webview_flutter_test.mocks.dart | 20 +++- 7 files changed, 201 insertions(+), 7 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 63d95c04cc9c..7b7e112b39fd 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.4.0 +* Adds support for the `loadFile` and `loadHtmlString` methods. * Updates example app Android compileSdkVersion to 31. * Integration test fixes. * Updates code for new analysis options. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 0a8928e34319..5c05e8f7de7d 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'dart:io'; 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())); @@ -28,6 +29,25 @@ The navigation delegate is set to block navigation to the youtube website. '''; +const String kLocalExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + +'''; + class WebViewExample extends StatefulWidget { @override _WebViewExampleState createState() => _WebViewExampleState(); @@ -133,6 +153,8 @@ enum MenuOptions { listCache, clearCache, navigationDelegate, + loadLocalFile, + loadHtmlString, } class SampleMenu extends StatelessWidget { @@ -171,6 +193,12 @@ class SampleMenu extends StatelessWidget { case MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -203,6 +231,14 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), ], ); }, @@ -279,6 +315,18 @@ class SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalExamplePage); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -292,6 +340,17 @@ class SampleMenu extends StatelessWidget { children: cookieWidgets.toList(), ); } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); + + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); + + return indexFile.path; + } } class NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 53fdd56af301..284a7a9d5a0d 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: flutter: sdk: flutter + path_provider: ^2.0.6 webview_flutter: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index b24020ba4dab..eb6ee4e09dc2 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -496,6 +496,35 @@ class WebViewController { WebView _widget; + /// Loads the file located at the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws an ArgumentError if the [absoluteFilePath] does not exist. + Future loadFile( + String absoluteFilePath, + ) { + assert(absoluteFilePath.isNotEmpty); + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + assert(html.isNotEmpty); + return _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 23693f3fbe6c..82f790a74478 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.3.1 +version: 2.4.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.3.1 + webview_flutter_android: ^2.4.0 webview_flutter_platform_interface: ^1.5.2 webview_flutter_wkwebview: ^2.4.0 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index f8a9e332c9d2..b05530272651 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -88,6 +88,98 @@ void main() { expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); }); + testWidgets('Load file', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadFile('/test/path/index.html'); + + verify(mockWebViewPlatformController.loadFile( + '/test/path/index.html', + )); + }); + + testWidgets('Load file with empty path', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadFile(''), throwsAssertionError); + }); + + testWidgets('Load HTML string without base URL', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadHtmlString('

This is a test paragraph.

'); + + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + )); + }); + + testWidgets('Load HTML string with base URL', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + ); + + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + )); + }); + + testWidgets('Load HTML string with empty string', + (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadHtmlString(''), throwsAssertionError); + }); + testWidgets('Load url', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart index b9d43cc6f182..8857f606f06d 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart @@ -1,7 +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. - // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter/test/webview_flutter_test.dart. // Do not manually edit this file. @@ -81,12 +77,28 @@ class MockWebViewPlatformController extends _i1.Mock _i1.throwOnMissingStub(this); } + @override + _i9.Future loadFile(String? absoluteFilePath) => + (super.noSuchMethod(Invocation.method(#loadFile, [absoluteFilePath]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future loadHtmlString(String? html, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); @override _i9.Future loadUrl(String? url, Map? headers) => (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override + _i9.Future loadRequest(_i4.WebViewRequest? request) => + (super.noSuchMethod(Invocation.method(#loadRequest, [request]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override _i9.Future updateSettings(_i4.WebSettings? setting) => (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), returnValue: Future.value(), From 17f95e9e7f74aa9599b37779ab632e5454d5f3f8 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 7 Dec 2021 10:09:05 +0100 Subject: [PATCH 222/259] [webview_flutter] Android implementation of `loadFlutterAsset` method. (#4581) --- .../webview_flutter_android/CHANGELOG.md | 4 + .../android/build.gradle | 4 - .../webviewflutter/FlutterAssetManager.java | 108 ++++++++++++++++++ .../FlutterAssetManagerHostApiImpl.java | 46 ++++++++ .../GeneratedAndroidWebView.java | 79 +++++++++++++ .../webviewflutter/WebViewFlutterPlugin.java | 14 ++- .../FlutterAssetManagerHostApiImplTest.java | 76 ++++++++++++ .../PluginBindingFlutterAssetManagerTest.java | 54 +++++++++ .../RegistrarFlutterAssetManagerTest.java | 55 +++++++++ .../example/assets/www/index.html | 20 ++++ .../example/assets/www/styles/style.css | 3 + .../example/lib/main.dart | 13 +++ .../example/lib/web_view.dart | 8 ++ .../example/pubspec.yaml | 2 + .../lib/src/android_webview.dart | 21 ++++ .../lib/src/android_webview.pigeon.dart | 67 +++++++++++ .../lib/webview_android_widget.dart | 38 +++++- .../pigeons/android_webview.dart | 7 ++ .../webview_flutter_android/pubspec.yaml | 4 +- .../test/android_webview.pigeon.dart | 53 +++++++++ .../test/android_webview_test.dart | 1 + .../test/android_webview_test.mocks.dart | 21 ++++ .../test/webview_android_widget_test.dart | 65 +++++++++++ .../webview_android_widget_test.mocks.dart | 22 ++++ 24 files changed, 775 insertions(+), 10 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/www/index.html create mode 100644 packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index caee5f7cb75a..e8d9e639c59d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + ## 2.5.0 * Adds an option to set the background color of the webview. diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index e70d4e68edc8..37954b36a834 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -58,8 +58,4 @@ android { } } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java new file mode 100644 index 000000000000..1d484d8639a0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry; +import java.io.IOException; + +/** Provides access to the assets registered as part of the App bundle. */ +abstract class FlutterAssetManager { + final AssetManager assetManager; + + /** + * Constructs a new instance of the {@link FlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within the + * App bundle. + */ + public FlutterAssetManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + /** + * Gets the relative file path to the Flutter asset with the given name, including the file's + * extension, e.g., "myImage.jpg". + * + *

The returned file path is relative to the Android app's standard asset's directory. + * Therefore, the returned path is appropriate to pass to Android's AssetManager, but the path is + * not appropriate to load as an absolute path. + */ + abstract String getAssetFilePathByName(String name); + + /** + * Returns a String array of all the assets at the given path. + * + * @param path A relative path within the assets, i.e., "docs/home.html". This value cannot be + * null. + * @return String[] Array of strings, one for each asset. These file names are relative to 'path'. + * This value may be null. + * @throws IOException Throws an IOException in case I/O operations were interrupted. + */ + public String[] list(@NonNull String path) throws IOException { + return assetManager.list(path); + } + + /** + * Provides access to assets using the {@link PluginRegistry.Registrar} for looking up file paths + * to Flutter assets. + * + * @deprecated The {@link RegistrarFlutterAssetManager} is for Flutter's v1 embedding. For + * instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Deprecated + static class RegistrarFlutterAssetManager extends FlutterAssetManager { + final PluginRegistry.Registrar registrar; + + /** + * Constructs a new instance of the {@link RegistrarFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param registrar Instance of {@link io.flutter.plugin.common.PluginRegistry.Registrar} used + * to look up file paths to assets registered by Flutter. + */ + RegistrarFlutterAssetManager(AssetManager assetManager, PluginRegistry.Registrar registrar) { + super(assetManager); + this.registrar = registrar; + } + + @Override + public String getAssetFilePathByName(String name) { + return registrar.lookupKeyForAsset(name); + } + } + + /** + * Provides access to assets using the {@link FlutterPlugin.FlutterAssets} for looking up file + * paths to Flutter assets. + */ + static class PluginBindingFlutterAssetManager extends FlutterAssetManager { + final FlutterPlugin.FlutterAssets flutterAssets; + + /** + * Constructs a new instance of the {@link PluginBindingFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param flutterAssets Instance of {@link + * io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets} used to look up file + * paths to assets registered by Flutter. + */ + PluginBindingFlutterAssetManager( + AssetManager assetManager, FlutterPlugin.FlutterAssets flutterAssets) { + super(assetManager); + this.flutterAssets = flutterAssets; + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssets.getAssetFilePathByName(name); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java new file mode 100644 index 000000000000..791912adb815 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Host api implementation for {@link WebView}. + * + *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. + */ +public class FlutterAssetManagerHostApiImpl implements FlutterAssetManagerHostApi { + final FlutterAssetManager flutterAssetManager; + + /** Constructs a new instance of {@link FlutterAssetManagerHostApiImpl}. */ + public FlutterAssetManagerHostApiImpl(FlutterAssetManager flutterAssetManager) { + this.flutterAssetManager = flutterAssetManager; + } + + @Override + public List list(String path) { + try { + String[] paths = flutterAssetManager.list(path); + + if (paths == null) { + return new ArrayList<>(); + } + + return Arrays.asList(paths); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssetManager.getAssetFilePathByName(name); + } +} 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 a5632d351f83..0123790ee799 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 @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** Generated class from Pigeon. */ @@ -1915,6 +1916,84 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { } } + private static class FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + public static final FlutterAssetManagerHostApiCodec INSTANCE = + new FlutterAssetManagerHostApiCodec(); + + private FlutterAssetManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface FlutterAssetManagerHostApi { + List list(String path); + + String getAssetFilePathByName(String name); + + /** The codec used by FlutterAssetManagerHostApi. */ + static MessageCodec getCodec() { + return FlutterAssetManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FlutterAssetManagerHostApi.list", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String pathArg = (String) args.get(0); + if (pathArg == null) { + throw new NullPointerException("pathArg unexpectedly null."); + } + List output = api.list(pathArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String nameArg = (String) args.get(0); + if (nameArg == null) { + throw new NullPointerException("nameArg unexpectedly null."); + } + String output = api.getAssetFilePathByName(nameArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { public static final WebChromeClientFlutterApiCodec INSTANCE = new WebChromeClientFlutterApiCodec(); 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 2b174ff35e6f..cbeda8dcf493 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 @@ -14,6 +14,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; @@ -61,7 +62,9 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.messenger(), registrar.platformViewRegistry(), registrar.activity(), - registrar.view()); + registrar.view(), + new FlutterAssetManager.RegistrarFlutterAssetManager( + registrar.context().getAssets(), registrar)); new FlutterCookieManager(registrar.messenger()); } @@ -69,7 +72,8 @@ private void setUp( BinaryMessenger binaryMessenger, PlatformViewRegistry viewRegistry, Context context, - View containerView) { + View containerView, + FlutterAssetManager flutterAssetManager) { new FlutterCookieManager(binaryMessenger); InstanceManager instanceManager = new InstanceManager(); @@ -111,6 +115,8 @@ private void setUp( binaryMessenger, new WebSettingsHostApiImpl( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + FlutterAssetManagerHostApi.setup( + binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); } @Override @@ -120,7 +126,9 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { binding.getBinaryMessenger(), binding.getPlatformViewRegistry(), binding.getApplicationContext(), - null); + null, + new FlutterAssetManager.PluginBindingFlutterAssetManager( + binding.getApplicationContext().getAssets(), binding.getFlutterAssets())); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java new file mode 100644 index 000000000000..f530365a9334 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class FlutterAssetManagerHostApiImplTest { + @Mock FlutterAssetManager mockFlutterAssetManager; + + FlutterAssetManagerHostApiImpl testFlutterAssetManagerHostApiImpl; + + @Before + public void setUp() { + mockFlutterAssetManager = mock(FlutterAssetManager.class); + + testFlutterAssetManagerHostApiImpl = + new FlutterAssetManagerHostApiImpl(mockFlutterAssetManager); + } + + @Test + public void list() { + try { + when(mockFlutterAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void list_returns_empty_list_when_no_results() { + try { + when(mockFlutterAssetManager.list("test/path")).thenReturn(null); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test(expected = RuntimeException.class) + public void list_should_convert_io_exception_to_runtime_exception() { + try { + when(mockFlutterAssetManager.list("test/path")).thenThrow(new IOException()); + testFlutterAssetManagerHostApiImpl.list("test/path"); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void getAssetFilePathByName() { + when(mockFlutterAssetManager.getAssetFilePathByName("index.html")) + .thenReturn("flutter_assets/index.html"); + String filePath = testFlutterAssetManagerHostApiImpl.getAssetFilePathByName("index.html"); + verify(mockFlutterAssetManager).getAssetFilePathByName("index.html"); + assertEquals("flutter_assets/index.html", filePath); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java new file mode 100644 index 000000000000..1f556b7bd486 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.PluginBindingFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class PluginBindingFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock FlutterAssets mockFlutterAssets; + + PluginBindingFlutterAssetManager tesPluginBindingFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockFlutterAssets = mock(FlutterAssets.class); + + tesPluginBindingFlutterAssetManager = + new PluginBindingFlutterAssetManager(mockAssetManager, mockFlutterAssets); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = tesPluginBindingFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + tesPluginBindingFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockFlutterAssets).getAssetFilePathByName("sample_movie.mp4"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java new file mode 100644 index 000000000000..86b0fb5432b9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.RegistrarFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +@SuppressWarnings("deprecation") +public class RegistrarFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock Registrar mockRegistrar; + + RegistrarFlutterAssetManager testRegistrarFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockRegistrar = mock(Registrar.class); + + testRegistrarFlutterAssetManager = + new RegistrarFlutterAssetManager(mockAssetManager, mockRegistrar); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = testRegistrarFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + testRegistrarFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockRegistrar).lookupKeyForAsset("sample_movie.mp4"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file 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 0c04c8ca4004..3bd283c3e712 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -185,6 +185,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadFlutterAsset, loadLocalFile, loadHtmlString, transparentBackground, @@ -226,6 +227,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case _MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; @@ -267,6 +271,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), const PopupMenuItem<_MenuOptions>( value: _MenuOptions.loadHtmlString, child: Text('Load HTML string'), @@ -357,6 +365,11 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + Future _onLoadLocalFileExample( WebViewController controller, BuildContext context) async { final String pathToIndex = await _prepareLocalFile(); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 395966bdd744..b32deab05477 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -382,6 +382,14 @@ class WebViewController { return _webViewPlatformController.loadFile(absoluteFilePath); } + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the supplied HTML string. /// /// The [baseUrl] parameter is used when resolving relative URLs within the diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 59579df8ca50..85990bd02139 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -34,3 +34,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css 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 ecd6f33d1e63..a7561cea687c 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 @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; 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 @@ -770,3 +771,23 @@ class WebResourceError { /// Describes the error. final String description; } + +/// Manages Flutter assets that are part of Android's app bundle. +class FlutterAssetManager { + /// Constructs the [FlutterAssetManager]. + const FlutterAssetManager(); + + /// Pigeon Host Api implementation for [FlutterAssetManager]. + @visibleForTesting + static FlutterAssetManagerHostApi api = FlutterAssetManagerHostApi(); + + /// Lists all assets at the given path. + /// + /// The assets are returned as a `List`. The `List` only + /// contains files which are direct childs + Future> list(String path) => api.list(path); + + /// Gets the relative file path to the Flutter asset with the given name. + Future getAssetFilePathByName(String name) => + api.getAssetFilePathByName(name); +} 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 ae528a64bb8f..f93685611ba3 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 @@ -1616,6 +1616,73 @@ class WebChromeClientHostApi { } } +class _FlutterAssetManagerHostApiCodec extends StandardMessageCodec { + const _FlutterAssetManagerHostApiCodec(); +} + +class FlutterAssetManagerHostApi { + /// Constructor for [FlutterAssetManagerHostApi]. 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. + FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FlutterAssetManagerHostApiCodec(); + + Future> list(String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (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 getAssetFilePathByName(String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (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?)!; + } + } +} + class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { const _WebChromeClientFlutterApiCodec(); } 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 0bfa04fde095..25f98742c119 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 @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; - import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart' as android_webview; @@ -20,6 +19,8 @@ class WebViewAndroidWidget extends StatefulWidget { required this.javascriptChannelRegistry, required this.onBuildWidget, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }); /// Initial parameters used to setup the WebView. @@ -47,6 +48,11 @@ class WebViewAndroidWidget extends StatefulWidget { /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Callback to build a widget once [android_webview.WebView] has been initialized. final Widget Function(WebViewAndroidPlatformController controller) onBuildWidget; @@ -67,6 +73,7 @@ class _WebViewAndroidWidgetState extends State { callbacksHandler: widget.callbacksHandler, javascriptChannelRegistry: widget.javascriptChannelRegistry, webViewProxy: widget.webViewProxy, + flutterAssetManager: widget.flutterAssetManager, ); } @@ -91,6 +98,8 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { required this.callbacksHandler, required this.javascriptChannelRegistry, @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), }) : assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { webView = webViewProxy.createWebView( @@ -134,6 +143,11 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { /// This should only be changed for testing purposes. final WebViewProxy webViewProxy; + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + /// Receives callbacks when content should be downloaded instead. @visibleForTesting late final WebViewAndroidDownloadListener downloadListener = @@ -166,6 +180,28 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { return webView.loadUrl(url, {}); } + @override + Future loadFlutterAsset(String key) async { + final String assetFilePath = + await flutterAssetManager.getAssetFilePathByName(key); + final List pathElements = assetFilePath.split('/'); + final String fileName = pathElements.removeLast(); + final List paths = + await flutterAssetManager.list(pathElements.join('/')); + + if (!paths.contains(fileName)) { + throw ArgumentError( + 'Asset for key "$key" not found.', + 'key', + ); + } + + return webView.loadUrl( + 'file:///android_asset/$assetFilePath', + {}, + ); + } + @override Future loadUrl( String url, 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 0fdec2c17756..78672ea5b671 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -193,6 +193,13 @@ abstract class WebChromeClientHostApi { void create(int instanceId, int webViewClientInstanceId); } +@HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') +abstract class FlutterAssetManagerHostApi { + List list(String path); + + String getAssetFilePathByName(String name); +} + @FlutterApi() abstract class WebChromeClientFlutterApi { void dispose(int instanceId); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 75245a31e4c2..bbe9ee174162 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.5.0 +version: 2.6.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.7.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: build_runner: ^2.1.4 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 942e59a0b2b3..90c1474f6c67 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -1055,3 +1055,56 @@ abstract class TestWebChromeClientHostApi { } } } + +class _TestAssetManagerHostApiCodec extends StandardMessageCodec { + const _TestAssetManagerHostApiCodec(); +} + +abstract class TestAssetManagerHostApi { + static const MessageCodec codec = _TestAssetManagerHostApiCodec(); + + List list(String path); + String getAssetFilePathByName(String name); + static void setup(TestAssetManagerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null.'); + final List args = (message as List?)!; + final String? arg_path = (args[0] as String?); + assert(arg_path != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null, expected non-null String.'); + final List output = api.list(arg_path!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null.'); + final List args = (message as List?)!; + final String? arg_name = (args[0] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null, expected non-null String.'); + final String output = api.getAssetFilePathByName(arg_name!); + return {'result': output}; + }); + } + } + } +} 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 0e7d5fcd552e..b903678ecef8 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 @@ -22,6 +22,7 @@ import 'android_webview_test.mocks.dart'; TestWebSettingsHostApi, TestWebViewClientHostApi, TestWebViewHostApi, + TestAssetManagerHostApi, WebChromeClient, WebView, WebViewClient, 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 3c1cd610c136..a08019e112ef 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 @@ -340,6 +340,27 @@ class MockTestWebViewHostApi extends _i1.Mock String toString() => super.toString(); } +/// A class which mocks [TestAssetManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestAssetManagerHostApi extends _i1.Mock + implements _i3.TestAssetManagerHostApi { + MockTestAssetManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + List list(String? path) => + (super.noSuchMethod(Invocation.method(#list, [path]), + returnValue: []) as List); + @override + String getAssetFilePathByName(String? name) => + (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), + returnValue: '') as String); + @override + String toString() => super.toString(); +} + /// A class which mocks [WebChromeClient]. /// /// See the documentation for Mockito's code generation for more information. 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 460cb54bd393..f3867b313d7d 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 @@ -16,6 +16,7 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ + android_webview.FlutterAssetManager, android_webview.WebSettings, android_webview.WebView, WebViewAndroidDownloadListener, @@ -30,6 +31,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$WebViewAndroidWidget', () { + late MockFlutterAssetManager mockFlutterAssetManager; late MockWebView mockWebView; late MockWebSettings mockWebSettings; late MockWebViewProxy mockWebViewProxy; @@ -44,6 +46,7 @@ void main() { late WebViewAndroidPlatformController testController; setUp(() { + mockFlutterAssetManager = MockFlutterAssetManager(); mockWebView = MockWebView(); mockWebSettings = MockWebSettings(); when(mockWebView.settings).thenReturn(mockWebSettings); @@ -77,6 +80,7 @@ void main() { callbacksHandler: mockCallbacksHandler, javascriptChannelRegistry: mockJavascriptChannelRegistry, webViewProxy: mockWebViewProxy, + flutterAssetManager: mockFlutterAssetManager, onBuildWidget: (WebViewAndroidPlatformController controller) { testController = controller; return Container(); @@ -299,6 +303,67 @@ void main() { )); }); + testWidgets('loadFlutterAsset', (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets('loadFlutterAsset with file in root', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets')).thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets( + 'loadFlutterAsset throws ArgumentError when asset does not exists', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer((_) => Future>.value([''])); + + expect( + () => testController.loadFlutterAsset(assetKey), + throwsA( + isA() + .having((ArgumentError error) => error.name, 'name', 'key') + .having((ArgumentError error) => error.message, 'message', + 'Asset for key "$assetKey" not found.'), + ), + ); + }); + testWidgets('loadHtmlString without base URL', (WidgetTester tester) async { await buildWidget(tester); 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 6ee53f9f9362..f3b06ea0a0bb 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 @@ -27,6 +27,28 @@ class _FakeJavascriptChannelRegistry_1 extends _i1.Fake class _FakeWebView_2 extends _i1.Fake implements _i2.WebView {} +/// A class which mocks [FlutterAssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterAssetManager extends _i1.Mock + implements _i2.FlutterAssetManager { + MockFlutterAssetManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future> list(String? path) => + (super.noSuchMethod(Invocation.method(#list, [path]), + returnValue: Future>.value([])) + as _i4.Future>); + @override + _i4.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), + returnValue: Future.value('')) as _i4.Future); + @override + String toString() => super.toString(); +} + /// A class which mocks [WebSettings]. /// /// See the documentation for Mockito's code generation for more information. From 2681f505dd8cd22fef97593354b4397b56ff9308 Mon Sep 17 00:00:00 2001 From: e-adrien Date: Tue, 7 Dec 2021 12:34:04 +0000 Subject: [PATCH 223/259] [webview_flutter] Add an option to set the background color of the webview (#3431) --- .../webview_flutter/CHANGELOG.md | 4 ++ .../webview_flutter/example/lib/main.dart | 38 +++++++++++++++++++ .../webview_flutter/lib/src/webview.dart | 8 ++++ .../webview_flutter/pubspec.yaml | 8 ++-- .../test/webview_flutter_test.dart | 28 ++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 7b7e112b39fd..1f8a33db4cb4 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.5.0 + +* Adds an option to set the background color of the webview. + ## 2.4.0 * Adds support for the `loadFile` and `loadHtmlString` methods. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 5c05e8f7de7d..c870ae9d829d 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -48,6 +48,27 @@ const String kLocalExamplePage = ''' '''; +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + class WebViewExample extends StatefulWidget { @override _WebViewExampleState createState() => _WebViewExampleState(); @@ -68,6 +89,7 @@ class _WebViewExampleState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.green, appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -106,6 +128,7 @@ class _WebViewExampleState extends State { print('Page finished loading: $url'); }, gestureNavigationEnabled: true, + backgroundColor: const Color(0x00000000), ); }), floatingActionButton: favoriteButton(), @@ -155,6 +178,7 @@ enum MenuOptions { navigationDelegate, loadLocalFile, loadHtmlString, + transparentBackground, } class SampleMenu extends StatelessWidget { @@ -170,6 +194,7 @@ class SampleMenu extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot controller) { return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), onSelected: (MenuOptions value) { switch (value) { case MenuOptions.showUserAgent: @@ -199,6 +224,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.loadHtmlString: _onLoadHtmlStringExample(controller.data!, context); break; + case MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -239,6 +267,11 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.loadLocalFile, child: Text('Load local file'), ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), ], ); }, @@ -327,6 +360,11 @@ class SampleMenu extends StatelessWidget { await controller.loadHtmlString(kLocalExamplePage); } + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index eb6ee4e09dc2..b2cc69045e5f 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -94,6 +94,7 @@ class WebView extends StatefulWidget { this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -286,6 +287,12 @@ class WebView extends StatefulWidget { /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override State createState() => _WebViewState(); } @@ -357,6 +364,7 @@ CreationParams _creationParamsfromWidget(WebView widget) { javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), userAgent: widget.userAgent, autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + backgroundColor: widget.backgroundColor, ); } diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 82f790a74478..6c00f65d8022 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.4.0 +version: 2.5.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.4.0 - webview_flutter_platform_interface: ^1.5.2 - webview_flutter_wkwebview: ^2.4.0 + webview_flutter_android: ^2.5.0 + webview_flutter_platform_interface: ^1.7.0 + webview_flutter_wkwebview: ^2.5.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index b05530272651..da860f0118d4 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -1020,6 +1020,34 @@ void main() { }); }); + group('Background color', () { + testWidgets('Defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, null); + }); + + testWidgets('Can be transparent', (WidgetTester tester) async { + const Color transparentColor = Color(0x00000000); + + await tester.pumpWidget(const WebView( + backgroundColor: transparentColor, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, transparentColor); + }); + }); + group('Custom platform implementation', () { setUp(() { WebView.platform = MyWebViewPlatform(); From 3ce611e4d2980696b95e994ac3a9bffc3897f0db Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Dec 2021 10:02:24 -0500 Subject: [PATCH 224/259] [flutter_plugin_tools] Improve package targeting (#4577) Improve package targeting: - `--run-on-changed-packages` now includes only changed packages in a federated plugin, not all packages. Include all packages isn't useful since (without changes that would cause them to be included anyway) package dependencies are not path-based between packages. - `--packages` now allows specifying package names of federated plugins. E.g., `--packages=path_provider_ios` will now work, instead of requiring `--packages=path_provider/path_provider_ios`. The fully qualified form still works as well (and is still needed for app-facing packages to disambiguate from the plugin group). Fixes https://github.com/flutter/flutter/issues/94618 --- script/tool/CHANGELOG.md | 5 ++ script/tool/README.md | 9 +- .../tool/lib/src/common/plugin_command.dart | 67 +++++++++++---- .../tool/test/common/plugin_command_test.dart | 85 +++++++++++++++++-- 4 files changed, 139 insertions(+), 27 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index dec218e2ba24..72539c24c5fc 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,11 @@ dependencies to path-based dependencies. - Adds a (hidden) `--run-on-dirty-packages` flag for use with `make-deps-path-based` in CI. +- `--packages` now allows using a federated plugin's package as a target without + fully specifying it (if it is not the same as the plugin's name). E.g., + `--packages=path_provide_ios` now works. +- `--run-on-changed-packages` now includes only the changed packages in a + federated plugin, not all packages in that plugin. - Fix `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). diff --git a/script/tool/README.md b/script/tool/README.md index 1a87f098757b..613cb5456e0a 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -51,8 +51,13 @@ following shows a number of common commands being run for a specific plugin. All examples assume running from source; see above for running the published version instead. -Note that the `plugins` argument, despite the name, applies to any package. -(It will likely be renamed `packages` in the future.) +Most commands take a `--packages` argument to control which package(s) the +command is targetting. An package name can be any of: +- The name of a package (e.g., `path_provider_android`). +- The name of a federated plugin (e.g., `path_provider`), in which case all + packages that make up that plugin will be targetted. +- A combination federated_plugin_name/package_name (e.g., + `path_provider/path_provider` for the app-facing package). ### Format Code diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 7166c754e129..184663568224 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -339,7 +339,7 @@ abstract class PluginCommand extends Command { final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { - packages = _getChangedPackages(changedFiles); + packages = _getChangedPackageNames(changedFiles); } } else if (getBoolArg(_runOnDirtyPackagesArg)) { final GitVersionFinder gitVersionFinder = @@ -348,7 +348,7 @@ abstract class PluginCommand extends Command { // _changesRequireFullTest is deliberately not used here, as this flag is // intended for use in CI to re-test packages changed by // 'make-deps-path-based'. - packages = _getChangedPackages( + packages = _getChangedPackageNames( await gitVersionFinder.getChangedFiles(includeUncommitted: true)); // For the same reason, empty is not treated as "all packages" as it is // for other flags. @@ -379,21 +379,23 @@ abstract class PluginCommand extends Command { await for (final FileSystemEntity subdir in entity.list(followLinks: false)) { if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - path.relative(subdir.path, from: dir.path); - final String packageName = path.basename(subdir.path); - final String basenamePath = path.basename(entity.path); + // There are three ways for a federated plugin to match: + // - package name (path_provider_android) + // - fully specified name (path_provider/path_provider_android) + // - group name (path_provider), which matches all packages in + // the group + final Set possibleMatches = { + path.basename(subdir.path), // package name + path.basename(entity.path), // group name + path.relative(subdir.path, from: dir.path), // fully specified + }; if (packages.isEmpty || - packages.contains(relativePath) || - packages.contains(basenamePath)) { + packages.intersection(possibleMatches).isNotEmpty) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), - excluded: excludedPluginNames.contains(basenamePath) || - excludedPluginNames.contains(packageName) || - excludedPluginNames.contains(relativePath)); + excluded: excludedPluginNames + .intersection(possibleMatches) + .isNotEmpty); } } } @@ -454,17 +456,48 @@ abstract class PluginCommand extends Command { return gitVersionFinder; } - // Returns packages that have been changed given a list of changed files. + // Returns the names of packages that have been changed given a list of + // changed files. + // + // The names will either be the actual package names, or potentially + // group/name specifiers (for example, path_provider/path_provider) for + // packages in federated plugins. // // The paths must use POSIX separators (e.g., as provided by git output). - Set _getChangedPackages(List changedFiles) { + Set _getChangedPackageNames(List changedFiles) { final Set packages = {}; + + // A helper function that returns true if candidatePackageName looks like an + // implementation package of a plugin called pluginName. Used to determine + // if .../packages/parentName/candidatePackageName/... + // looks like a path in a federated plugin package (candidatePackageName) + // rather than a top-level package (parentName). + bool isFederatedPackage(String candidatePackageName, String parentName) { + return candidatePackageName == parentName || + candidatePackageName.startsWith('${parentName}_'); + } + for (final String path in changedFiles) { final List pathComponents = p.posix.split(path); final int packagesIndex = pathComponents.indexWhere((String element) => element == 'packages'); if (packagesIndex != -1) { - packages.add(pathComponents[packagesIndex + 1]); + // Find the name of the directory directly under packages. This is + // either the name of the package, or a plugin group directory for + // a federated plugin. + final String topLevelName = pathComponents[packagesIndex + 1]; + String packageName = topLevelName; + if (packagesIndex + 2 < pathComponents.length && + isFederatedPackage( + pathComponents[packagesIndex + 2], topLevelName)) { + // This looks like a federated package; use the full specifier if + // the name would be ambiguous (i.e., for the app-facing package). + packageName = pathComponents[packagesIndex + 2]; + if (packageName == topLevelName) { + packageName = '$topLevelName/$packageName'; + } + } + packages.add(packageName); } } if (packages.isEmpty) { diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 222df544f344..28a03c61d59f 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -180,6 +180,79 @@ void main() { expect(command.plugins, unorderedEquals([])); }); + test( + 'explicitly specifying the plugin (group) name of a federated plugin ' + 'should include all plugins in the group', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final Directory appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + final Directory platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + final Directory implementationPackage = + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint( + runner, ['sample', '--base-sha=main', '--packages=plugin1']); + + expect( + command.plugins, + unorderedEquals([ + appFacingPackage.path, + platformInterfacePackage.path, + implementationPackage.path + ])); + }); + + test( + 'specifying the app-facing package of a federated plugin using its ' + 'fully qualified name should include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final Directory appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); + + expect(command.plugins, unorderedEquals([appFacingPackage.path])); + }); + + test( + 'specifying a package of a federated plugin by its name should ' + 'include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + + createFakePlugin('plugin1', pluginGroup); + final Directory platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=main', + '--packages=plugin1_platform_interface' + ]); + + expect(command.plugins, + unorderedEquals([platformInterfacePackage.path])); + }); + group('conflicting package selection', () { test('does not allow --packages with --run-on-changed-packages', () async { @@ -442,7 +515,7 @@ packages/plugin1/plugin1_web/plugin1_web.dart }); test( - 'changing one plugin in a federated group should include all plugins in the group', + 'changing one plugin in a federated group should only include that plugin', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' @@ -451,17 +524,13 @@ packages/plugin1/plugin1/plugin1.dart ]; final Directory plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + createFakePlugin('plugin1_platform_interface', packagesDir.childDirectory('plugin1')); - final Directory plugin3 = createFakePlugin( - 'plugin1_web', packagesDir.childDirectory('plugin1')); + createFakePlugin('plugin1_web', packagesDir.childDirectory('plugin1')); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); - expect( - command.plugins, - unorderedEquals( - [plugin1.path, plugin2.path, plugin3.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { From 0f0dcbf10e9cdd2a61a6538eb80378f896e42c79 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 7 Dec 2021 16:07:50 +0100 Subject: [PATCH 225/259] [webview_flutter] Implement loadRequest in Android package. (#4563) Implements the `loadRequest` method added in #4450 for the Android package. Related issue: - flutter/flutter#27730 --- .../webview_flutter_android/CHANGELOG.md | 4 + .../GeneratedAndroidWebView.java | 35 ++++++ .../webviewflutter/WebViewHostApiImpl.java | 6 + .../plugins/webviewflutter/WebViewTest.java | 6 + .../example/lib/main.dart | 19 +++ .../example/lib/web_view.dart | 5 + .../lib/src/android_webview.dart | 8 ++ .../lib/src/android_webview.pigeon.dart | 27 +++++ .../lib/src/android_webview_api_impls.dart | 11 ++ .../lib/webview_android_widget.dart | 23 ++++ .../pigeons/android_webview.dart | 6 + .../webview_flutter_android/pubspec.yaml | 2 +- .../test/android_webview.pigeon.dart | 26 ++++ .../test/android_webview_test.mocks.dart | 114 ++++++++++-------- .../test/webview_android_widget_test.dart | 76 ++++++++++++ .../webview_android_widget_test.mocks.dart | 26 ++-- 16 files changed, 335 insertions(+), 59 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index e8d9e639c59d..ce73ab26cad0 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.0 + +* Adds support for the `loadRequest` method from the platform interface. + ## 2.6.0 * Adds implementation of the `loadFlutterAsset` method from the platform interface. 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 0123790ee799..0e6776ecd618 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 @@ -186,6 +186,8 @@ void loadDataWithBaseUrl( void loadUrl(Long instanceId, String url, Map headers); + void postUrl(Long instanceId, String url, byte[] data); + String getUrl(Long instanceId); Boolean canGoBack(Long instanceId); @@ -410,6 +412,39 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.postUrl", 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."); + } + String urlArg = (String) args.get(1); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + byte[] dataArg = (byte[]) args.get(2); + if (dataArg == null) { + throw new NullPointerException("dataArg unexpectedly null."); + } + api.postUrl(instanceIdArg.longValue(), urlArg, dataArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( 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 78b06aac90b1..0f3161355dcb 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 @@ -382,6 +382,12 @@ public void loadUrl(Long instanceId, String url, Map headers) { webView.loadUrl(url, headers); } + @Override + public void postUrl(Long instanceId, String url, byte[] data) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.postUrl(url, data); + } + @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); 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 dc58b9b100ff..2312b764342f 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 @@ -208,6 +208,12 @@ public void loadUrl() { verify(mockWebView).loadUrl("https://www.google.com", new HashMap<>()); } + @Test + public void postUrl() { + testHostApiImpl.postUrl(0L, "https://www.google.com", new byte[] {0x01, 0x02}); + verify(mockWebView).postUrl("https://www.google.com", new byte[] {0x01, 0x02}); + } + @Test public void getUrl() { when(mockWebView.getUrl()).thenReturn("https://www.google.com"); 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 3bd283c3e712..eb3162a179ff 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -7,6 +7,7 @@ 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'; @@ -189,6 +190,7 @@ enum _MenuOptions { loadLocalFile, loadHtmlString, transparentBackground, + doPostRequest, } class _SampleMenu extends StatelessWidget { @@ -239,6 +241,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.transparentBackground: _onTransparentBackground(controller.data!, context); break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -288,6 +293,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), ], ); }, @@ -382,6 +391,16 @@ class _SampleMenu extends StatelessWidget { await controller.loadHtmlString(kExamplePage); } + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index b32deab05477..adaf7fbb3ee8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -418,6 +418,11 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. 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 a7561cea687c..d02701643291 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 @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -169,6 +170,13 @@ class WebView { return api.loadUrlFromInstance(this, url, headers); } + /// Loads the URL with postData using "POST" method into this WebView. + /// + /// If url is not a network URL, it will be loaded with [loadUrl] instead, ignoring the postData param. + Future postUrl(String url, Uint8List data) { + return api.postUrlFromInstance(this, url, data); + } + /// Gets the URL for the current page. /// /// This is not always the same as the URL passed to 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 f93685611ba3..e0a1db311b74 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 @@ -220,6 +220,33 @@ class WebViewHostApi { } } + Future postUrl( + int arg_instanceId, String arg_url, Uint8List arg_data) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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 = + (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.WebViewHostApi.getUrl', codec, 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 51efdaadebdc..1db5ed449c56 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 @@ -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:typed_data'; + import 'package:flutter/services.dart'; import 'android_webview.dart'; @@ -152,6 +154,15 @@ class WebViewHostApiImpl extends WebViewHostApi { return loadUrl(instanceManager.getInstanceId(instance)!, url, headers); } + /// Helper method to convert instances ids to objects. + Future postUrlFromInstance( + WebView instance, + String url, + Uint8List data, + ) { + return postUrl(instanceManager.getInstanceId(instance)!, url, data); + } + /// Helper method to convert instances ids to objects. Future getUrlFromInstance(WebView instance) { return getUrl(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 25f98742c119..fc295be65aa3 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 @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; @@ -210,6 +211,28 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { return webView.loadUrl(url, headers ?? {}); } + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + @override + Future loadRequest( + WebViewRequest request, + ) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + switch (request.method) { + case WebViewRequestMethod.get: + return webView.loadUrl(request.uri.toString(), request.headers); + case WebViewRequestMethod.post: + return webView.postUrl( + request.uri.toString(), request.body ?? Uint8List(0)); + default: + throw UnimplementedError( + 'This version of webview_android_widget currently has no implementation for HTTP method ${request.method.serialize()} in loadRequest.', + ); + } + } + @override Future currentUrl() => webView.getUrl(); 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 78672ea5b671..4907dadb41b7 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -46,6 +46,12 @@ abstract class WebViewHostApi { Map headers, ); + void postUrl( + int instanceId, + String url, + Uint8List data, + ); + String getUrl(int instanceId); bool canGoBack(int instanceId); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index bbe9ee174162..08bcbb50602e 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.6.0 +version: 2.7.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 90c1474f6c67..1e47c79d32b7 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -27,6 +27,7 @@ abstract class TestWebViewHostApi { 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); bool canGoBack(int instanceId); bool canGoForward(int instanceId); @@ -180,6 +181,31 @@ abstract class TestWebViewHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl 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.WebViewHostApi.postUrl was null, expected non-null int.'); + final String? arg_url = (args[1] as String?); + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null String.'); + final Uint8List? arg_data = (args[2] as Uint8List?); + assert(arg_data != null, + 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null Uint8List.'); + api.postUrl(arg_instanceId!, arg_url!, arg_data!); + return {}; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, 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 a08019e112ef..8bb569497b59 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 @@ -1,9 +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. + // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; -import 'dart:ui' as _i5; +import 'dart:async' as _i5; +import 'dart:typed_data' as _i4; +import 'dart:ui' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; @@ -241,6 +246,10 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#loadUrl, [instanceId, url, headers]), returnValueForMissingStub: null); @override + void postUrl(int? instanceId, String? url, _i4.Uint8List? data) => + super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), + returnValueForMissingStub: null); + @override String getUrl(int? instanceId) => (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), returnValue: '') as String); @@ -270,12 +279,12 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#clearCache, [instanceId, includeDiskFiles]), returnValueForMissingStub: null); @override - _i4.Future evaluateJavascript( + _i5.Future evaluateJavascript( int? instanceId, String? javascriptString) => (super.noSuchMethod( Invocation.method( #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value('')) as _i4.Future); + returnValue: Future.value('')) as _i5.Future); @override String getTitle(int? instanceId) => (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), @@ -394,15 +403,15 @@ class MockWebView extends _i1.Mock implements _i2.WebView { (super.noSuchMethod(Invocation.getter(#settings), returnValue: _FakeWebSettings_0()) as _i2.WebSettings); @override - _i4.Future loadData( + _i5.Future loadData( {String? data, String? mimeType, String? encoding}) => (super.noSuchMethod( Invocation.method(#loadData, [], {#data: data, #mimeType: mimeType, #encoding: encoding}), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future loadDataWithBaseUrl( + _i5.Future loadDataWithBaseUrl( {String? baseUrl, String? data, String? mimeType, @@ -417,109 +426,114 @@ class MockWebView extends _i1.Mock implements _i2.WebView { #historyUrl: historyUrl }), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future loadUrl(String? url, Map? headers) => + _i5.Future loadUrl(String? url, Map? headers) => (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future postUrl(String? url, _i4.Uint8List? data) => + (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future getUrl() => + _i5.Future getUrl() => (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i4.Future); + returnValue: Future.value()) as _i5.Future); @override - _i4.Future canGoBack() => + _i5.Future canGoBack() => (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i4.Future); + returnValue: Future.value(false)) as _i5.Future); @override - _i4.Future canGoForward() => + _i5.Future canGoForward() => (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i4.Future); + returnValue: Future.value(false)) as _i5.Future); @override - _i4.Future goBack() => + _i5.Future goBack() => (super.noSuchMethod(Invocation.method(#goBack, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future goForward() => + _i5.Future goForward() => (super.noSuchMethod(Invocation.method(#goForward, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future reload() => + _i5.Future reload() => (super.noSuchMethod(Invocation.method(#reload, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future clearCache(bool? includeDiskFiles) => + _i5.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future evaluateJavascript(String? javascriptString) => (super + _i5.Future evaluateJavascript(String? javascriptString) => (super .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i4.Future); + returnValue: Future.value()) as _i5.Future); @override - _i4.Future getTitle() => + _i5.Future getTitle() => (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i4.Future); + returnValue: Future.value()) as _i5.Future); @override - _i4.Future scrollTo(int? x, int? y) => + _i5.Future scrollTo(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future scrollBy(int? x, int? y) => + _i5.Future scrollBy(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future getScrollX() => + _i5.Future getScrollX() => (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i4.Future); + returnValue: Future.value(0)) as _i5.Future); @override - _i4.Future getScrollY() => + _i5.Future getScrollY() => (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i4.Future); + returnValue: Future.value(0)) as _i5.Future); @override - _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future addJavaScriptChannel( + _i5.Future addJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future removeJavaScriptChannel( + _i5.Future removeJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future setDownloadListener(_i2.DownloadListener? listener) => + _i5.Future setDownloadListener(_i2.DownloadListener? listener) => (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => + _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future setBackgroundColor(_i5.Color? color) => + _i5.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future release() => + _i5.Future release() => (super.noSuchMethod(Invocation.method(#release, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override String toString() => super.toString(); } 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 f3867b313d7d..c203ef04a2ce 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 @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -407,6 +408,81 @@ void main() { )); }); + 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, + )); + + verify(mockWebView.loadUrl( + 'https://www.google.com', + {}, + )); + }); + + 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'}, + )); + + verify(mockWebView.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + )); + }); + + testWidgets('POST without body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + )); + + verify(mockWebView.postUrl( + 'https://www.google.com', + Uint8List(0), + )); + }); + + testWidgets('POST with body', (WidgetTester tester) async { + await buildWidget(tester); + + final Uint8List body = Uint8List.fromList('Test Body'.codeUnits); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + body: body)); + + verify(mockWebView.postUrl( + 'https://www.google.com', + body, + )); + }); + }); + testWidgets('currentUrl', (WidgetTester tester) async { await buildWidget(tester); 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 f3b06ea0a0bb..ece17ad61cb8 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 @@ -1,13 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/webview_android_widget_test.dart. // Do not manually edit this file. import 'dart:async' as _i4; -import 'dart:ui' as _i5; +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; -import 'package:webview_flutter_android/webview_android_widget.dart' as _i6; +import 'package:webview_flutter_android/webview_android_widget.dart' as _i7; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' as _i3; @@ -165,6 +170,11 @@ class MockWebView extends _i1.Mock implements _i2.WebView { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override + _i4.Future postUrl(String? url, _i5.Uint8List? data) => + (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override _i4.Future getUrl() => (super.noSuchMethod(Invocation.method(#getUrl, []), returnValue: Future.value()) as _i4.Future); @@ -252,7 +262,7 @@ class MockWebView extends _i1.Mock implements _i2.WebView { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i4.Future setBackgroundColor(_i5.Color? color) => + _i4.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @@ -269,7 +279,7 @@ class MockWebView extends _i1.Mock implements _i2.WebView { /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidDownloadListener extends _i1.Mock - implements _i6.WebViewAndroidDownloadListener { + implements _i7.WebViewAndroidDownloadListener { MockWebViewAndroidDownloadListener() { _i1.throwOnMissingStub(this); } @@ -295,7 +305,7 @@ class MockWebViewAndroidDownloadListener extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidJavaScriptChannel extends _i1.Mock - implements _i6.WebViewAndroidJavaScriptChannel { + implements _i7.WebViewAndroidJavaScriptChannel { MockWebViewAndroidJavaScriptChannel() { _i1.throwOnMissingStub(this); } @@ -321,7 +331,7 @@ class MockWebViewAndroidJavaScriptChannel extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidWebChromeClient extends _i1.Mock - implements _i6.WebViewAndroidWebChromeClient { + implements _i7.WebViewAndroidWebChromeClient { MockWebViewAndroidWebChromeClient() { _i1.throwOnMissingStub(this); } @@ -338,7 +348,7 @@ class MockWebViewAndroidWebChromeClient extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewAndroidWebViewClient extends _i1.Mock - implements _i6.WebViewAndroidWebViewClient { + implements _i7.WebViewAndroidWebViewClient { MockWebViewAndroidWebViewClient() { _i1.throwOnMissingStub(this); } @@ -470,7 +480,7 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock /// A class which mocks [WebViewProxy]. /// /// See the documentation for Mockito's code generation for more information. -class MockWebViewProxy extends _i1.Mock implements _i6.WebViewProxy { +class MockWebViewProxy extends _i1.Mock implements _i7.WebViewProxy { MockWebViewProxy() { _i1.throwOnMissingStub(this); } From c6914515c6fd04b93360fcf0dae86d5d4d1409be Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 7 Dec 2021 19:00:07 +0100 Subject: [PATCH 226/259] [webview_flutter] Add loadRequest functionality to app facing package. (#4573) * Add android implementations for loadRequest. * Update changelog and pubspec. * Fix comment. * Fix comment. * Add tests * Add back removed license headers * Fix analysis error * Add support for loadRequest to app facing package * Add test * Comment pending dependency in pubspec * Remove workaround for supporting custom headers when making post requests on Android. * Document android workaround in dart doc * Enforce uri scheme * Update loadRequest dartdoc * Document android workaround in readme * Processed PR feedback. * Updated dependency, version and changelog. --- .../webview_flutter/CHANGELOG.md | 4 ++ .../webview_flutter/webview_flutter/README.md | 5 +++ .../webview_flutter/example/lib/main.dart | 20 ++++++++++ .../lib/platform_interface.dart | 4 +- .../webview_flutter/lib/src/webview.dart | 20 ++++++++++ .../webview_flutter/pubspec.yaml | 6 +-- .../test/webview_flutter_test.dart | 25 +++++++++++++ .../test/webview_flutter_test.mocks.dart | 37 ++++++++++++------- 8 files changed, 103 insertions(+), 18 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 1f8a33db4cb4..a9e133d08d47 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Adds support for the `loadRequest` method. + ## 2.5.0 * Adds an option to set the background color of the webview. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index de475ad9acd7..c3982eab2bad 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -92,3 +92,8 @@ android { To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). + +### 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 diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index c870ae9d829d..02e6ef85ec61 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; @@ -176,6 +177,7 @@ enum MenuOptions { listCache, clearCache, navigationDelegate, + doPostRequest, loadLocalFile, loadHtmlString, transparentBackground, @@ -218,6 +220,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; case MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; @@ -259,6 +264,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), const PopupMenuItem( value: MenuOptions.loadHtmlString, child: Text('Load HTML string'), @@ -348,6 +357,17 @@ class SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Future _onLoadLocalFileExample( WebViewController controller, BuildContext context) async { final String pathToIndex = await _prepareLocalFile(); diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart index aa7b3a0931e8..ab1cbb1bd344 100644 --- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart @@ -21,4 +21,6 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte WebSetting, WebSettings, WebResourceError, - WebResourceErrorType; + WebResourceErrorType, + WebViewRequest, + WebViewRequestMethod; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index b2cc69045e5f..d76f7b359482 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -550,6 +550,26 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Makes a specific HTTP request and loads the response in the webview. + /// + /// [WebViewRequest.method] must be one of the supported HTTP methods + /// in [WebViewRequestMethod]. + /// + /// If [WebViewRequest.headers] is not empty, its key-value pairs will be + /// added as the headers for the request. + /// + /// If [WebViewRequest.body] is not null, it will be added as the body + /// for the request. + /// + /// Throws an ArgumentError if [WebViewRequest.uri] has empty scheme. + /// + /// Android only: + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 6c00f65d8022..1bc6bab8b3ff 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.5.0 +version: 2.6.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,8 +19,8 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.5.0 - webview_flutter_platform_interface: ^1.7.0 + webview_flutter_android: ^2.7.0 + webview_flutter_platform_interface: ^1.8.0 webview_flutter_wkwebview: ^2.5.0 dev_dependencies: diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index da860f0118d4..d422402dbae7 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_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:typed_data'; + import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; @@ -246,6 +248,29 @@ void main() { )); }); + testWidgets('loadRequest', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(controller, isNotNull); + + final WebViewRequest req = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + + await controller!.loadRequest(req); + + verify(mockWebViewPlatformController.loadRequest(req)); + }); + testWidgets('Clear Cache', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart index 8857f606f06d..fe3060164df2 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart @@ -1,22 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter/test/webview_flutter_test.dart. // Do not manually edit this file. import 'dart:async' as _i9; -import 'package:flutter/foundation.dart' as _i7; +import 'package:flutter/foundation.dart' as _i3; import 'package:flutter/gestures.dart' as _i8; import 'package:flutter/widgets.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' - as _i6; + as _i7; import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart' - as _i3; + as _i4; import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' - as _i5; + as _i6; import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart' as _i10; -import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i4; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i5; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -29,14 +33,14 @@ import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i4; class _FakeWidget_0 extends _i1.Fake implements _i2.Widget { @override - String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); } /// A class which mocks [WebViewPlatform]. /// /// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatform extends _i1.Mock implements _i3.WebViewPlatform { +class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform { MockWebViewPlatform() { _i1.throwOnMissingStub(this); } @@ -44,11 +48,11 @@ class MockWebViewPlatform extends _i1.Mock implements _i3.WebViewPlatform { @override _i2.Widget build( {_i2.BuildContext? context, - _i4.CreationParams? creationParams, - _i5.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - _i6.JavascriptChannelRegistry? javascriptChannelRegistry, - _i3.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set<_i7.Factory<_i8.OneSequenceGestureRecognizer>>? + _i5.CreationParams? creationParams, + _i6.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + _i7.JavascriptChannelRegistry? javascriptChannelRegistry, + _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>? gestureRecognizers}) => (super.noSuchMethod( Invocation.method(#build, [], { @@ -83,6 +87,11 @@ class MockWebViewPlatformController extends _i1.Mock returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override + _i9.Future loadFlutterAsset(String? key) => + (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override _i9.Future loadHtmlString(String? html, {String? baseUrl}) => (super.noSuchMethod( Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), @@ -94,12 +103,12 @@ class MockWebViewPlatformController extends _i1.Mock returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override - _i9.Future loadRequest(_i4.WebViewRequest? request) => + _i9.Future loadRequest(_i5.WebViewRequest? request) => (super.noSuchMethod(Invocation.method(#loadRequest, [request]), returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override - _i9.Future updateSettings(_i4.WebSettings? setting) => + _i9.Future updateSettings(_i5.WebSettings? setting) => (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); From 60cd47ca648ce74c69e6cca3667a65047214387a Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 7 Dec 2021 19:44:05 +0100 Subject: [PATCH 227/259] [webview_flutter] Add Android implementations for new cookie manager, to allow setting cookies directly and on webview creation. (#4557) --- .../webview_flutter_android/CHANGELOG.md | 4 + .../CookieManagerHostApiImpl.java | 29 ++++ .../webviewflutter/FlutterCookieManager.java | 56 ------- .../GeneratedAndroidWebView.java | 88 +++++++++++ .../webviewflutter/WebViewFlutterPlugin.java | 14 +- .../CookieManagerHostApiImplTest.java | 83 ++++++++++ .../webviewflutter/utils/TestUtils.java | 47 ++++++ .../example/lib/main.dart | 19 ++- .../example/lib/web_view.dart | 25 +++ .../lib/src/android_webview.dart | 43 +++++ .../lib/src/android_webview.pigeon.dart | 66 ++++++++ .../lib/webview_android.dart | 8 +- .../lib/webview_android_cookie_manager.dart | 36 +++++ .../lib/webview_android_widget.dart | 9 ++ .../pigeons/android_webview.dart | 8 + .../webview_flutter_android/pubspec.yaml | 2 +- .../test/android_webview_test.dart | 17 ++ .../test/android_webview_test.mocks.dart | 147 ++++++++++-------- .../webview_android_cookie_manager_test.dart | 53 +++++++ ...iew_android_cookie_manager_test.mocks.dart | 38 +++++ 20 files changed, 660 insertions(+), 132 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java create mode 100644 packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index ce73ab26cad0..7bd33879cd7d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + ## 2.7.0 * Adds support for the `loadRequest` method from the platform interface. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java new file mode 100644 index 000000000000..3e38ce94b3a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.CookieManager; + +class CookieManagerHostApiImpl implements GeneratedAndroidWebView.CookieManagerHostApi { + @Override + public void clearCookies(GeneratedAndroidWebView.Result result) { + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(result::success); + } else { + final boolean hasCookies = cookieManager.hasCookies(); + if (hasCookies) { + cookieManager.removeAllCookie(); + } + result.success(hasCookies); + } + } + + @Override + public void setCookie(String url, String value) { + CookieManager.getInstance().setCookie(url, value); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21daadeb..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +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.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} 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 0e6776ecd618..8ef0b8d11f96 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 @@ -162,6 +162,94 @@ public interface Result { void error(Throwable error); } + private static class CookieManagerHostApiCodec extends StandardMessageCodec { + public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec(); + + private CookieManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CookieManagerHostApi { + void clearCookies(Result result); + + void setCookie(String url, String value); + + /** The codec used by CookieManagerHostApi. */ + static MessageCodec getCodec() { + return CookieManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `CookieManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CookieManagerHostApi.clearCookies", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + Result resultCallback = + new Result() { + public void success(Boolean result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.clearCookies(resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CookieManagerHostApi.setCookie", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String urlArg = (String) args.get(0); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + String valueArg = (String) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setCookie(urlArg, valueArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebViewHostApiCodec extends StandardMessageCodec { public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); 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 cbeda8dcf493..4ef622fe47cb 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 @@ -13,6 +13,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; @@ -30,7 +31,6 @@ */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { private FlutterPluginBinding pluginBinding; - private FlutterCookieManager flutterCookieManager; private WebViewHostApiImpl webViewHostApi; private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; @@ -65,7 +65,6 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.view(), new FlutterAssetManager.RegistrarFlutterAssetManager( registrar.context().getAssets(), registrar)); - new FlutterCookieManager(registrar.messenger()); } private void setUp( @@ -74,7 +73,6 @@ private void setUp( Context context, View containerView, FlutterAssetManager flutterAssetManager) { - new FlutterCookieManager(binaryMessenger); InstanceManager instanceManager = new InstanceManager(); @@ -117,6 +115,7 @@ private void setUp( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); FlutterAssetManagerHostApi.setup( binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); + CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); } @Override @@ -132,14 +131,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { } @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java new file mode 100644 index 000000000000..6daeb1be7f63 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugins.webviewflutter.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CookieManagerHostApiImplTest { + + private CookieManager cookieManager; + private MockedStatic staticMockCookieManager; + + @Before + public void setup() { + staticMockCookieManager = mockStatic(CookieManager.class); + cookieManager = mock(CookieManager.class); + when(CookieManager.getInstance()).thenReturn(cookieManager); + when(cookieManager.hasCookies()).thenReturn(true); + doAnswer( + answer -> { + ((ValueCallback) answer.getArgument(0)).onReceiveValue(true); + return null; + }) + .when(cookieManager) + .removeAllCookies(any()); + } + + @After + public void tearDown() { + staticMockCookieManager.close(); + } + + @Test + public void setCookieShouldCallSetCookie() { + // Setup + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.setCookie("flutter.dev", "foo=bar; path=/"); + // Verify + verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/"); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookies(any()); + verify(result).success(true); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookie(); + verify(result).success(true); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java new file mode 100644 index 000000000000..31e7d58ee13f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.junit.Assert; + +public class TestUtils { + public static void setFinalStatic(Class classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } + } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index eb3162a179ff..0d0cd59af796 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -191,6 +191,7 @@ enum _MenuOptions { loadHtmlString, transparentBackground, doPostRequest, + setCookie, } class _SampleMenu extends StatelessWidget { @@ -244,6 +245,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.doPostRequest: _onDoPostRequest(controller.data!, context); break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -297,6 +301,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.doPostRequest, child: Text('Post Request'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), ], ); }, @@ -356,7 +364,7 @@ class _SampleMenu extends StatelessWidget { Future _onClearCookies( WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebView.platform.clearCookies(); + final bool hadCookies = await WebViewCookieManager.instance.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; @@ -367,6 +375,15 @@ class _SampleMenu extends StatelessWidget { )); } + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Future _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index adaf7fbb3ee8..91ea66376904 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_decision.dart'; @@ -61,6 +63,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -104,6 +107,9 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -295,6 +301,7 @@ class _WebViewState extends State { autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -674,3 +681,21 @@ WebSettings _webSettingsFromWidget(WebView widget) { zoomEnabled: widget.zoomEnabled, ); } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_android.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +} 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 d02701643291..dfa05cd92ee6 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 @@ -375,6 +375,49 @@ class WebView { } } +/// Manages cookies globally for all webviews. +class CookieManager { + CookieManager._(); + + static CookieManager? _instance; + + /// Gets the globally set CookieManager instance. + static CookieManager get instance => _instance ??= CookieManager._(); + + /// Setter for the singleton value, for testing purposes only. + @visibleForTesting + static set instance(CookieManager value) => _instance = value; + + /// Pigeon Host Api implementation for [CookieManager]. + @visibleForTesting + static CookieManagerHostApi api = CookieManagerHostApi(); + + /// Sets a single cookie (key-value pair) for the given URL. Any existing + /// cookie with the same host, path and name will be replaced with the new + /// cookie. The cookie being set will be ignored if it is expired. To set + /// multiple cookies, your application should invoke this method multiple + /// times. + /// + /// The value parameter must follow the format of the Set-Cookie HTTP + /// response header defined by RFC6265bis. This is a key-value pair of the + /// form "key=value", optionally followed by a list of cookie attributes + /// delimited with semicolons (ex. "key=value; Max-Age=123"). Please consult + /// the RFC specification for a list of valid attributes. + /// + /// Note: if specifying a value containing the "Secure" attribute, url must + /// use the "https://" scheme. + /// + /// Params: + /// url – the URL for which the cookie is to be set + /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header + Future setCookie(String url, String value) => api.setCookie(url, value); + + /// Removes all cookies. + /// + /// The returned future resolves to true if any cookies were removed. + Future clearCookies() => api.clearCookies(); +} + /// Manages settings state for a [WebView]. /// /// When a WebView is first created, it obtains a set of default settings. These 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 e0a1db311b74..810a71732e7b 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 @@ -63,6 +63,72 @@ class WebResourceErrorData { } } +class _CookieManagerHostApiCodec extends StandardMessageCodec { + const _CookieManagerHostApiCodec(); +} + +class CookieManagerHostApi { + /// Constructor for [CookieManagerHostApi]. 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. + CookieManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CookieManagerHostApiCodec(); + + Future clearCookies() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 bool?)!; + } + } + + Future setCookie(String arg_url, String arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .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 = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart index ee474b9d43f8..1f0eb7bd7ded 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -65,5 +65,11 @@ class AndroidWebView implements WebViewPlatform { } @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart new file mode 100644 index 000000000000..bba75ff4f613 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() => + android_webview.CookieManager.instance.clearCookies(); + + @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 android_webview.CookieManager.instance.setCookie( + cookie.domain, + '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; 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 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} 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 fc295be65aa3..1dec9c105741 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart' as android_webview; @@ -363,6 +364,14 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { } addJavascriptChannels(creationParams.javascriptChannelNames); + + // TODO(BeMacized): Remove once platform implementations + // are able to register themselves (Flutter >=2.8), + // https://github.com/flutter/flutter/issues/94224 + WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager(); + + creationParams.cookies + .forEach(WebViewCookieManagerPlatform.instance!.setCookie); } Future _setHasProgressTracking(bool hasProgressTracking) 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 4907dadb41b7..36862f7cbacc 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -18,6 +18,14 @@ class WebResourceErrorData { String? description; } +@HostApi() +abstract class CookieManagerHostApi { + @async + bool clearCookies(); + + void setCookie(String url, String value); +} + @HostApi(dartHostTestHandler: 'TestWebViewHostApi') abstract class WebViewHostApi { void create(int instanceId, bool useHybridComposition); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 08bcbb50602e..34bea570ae43 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.7.0 +version: 2.8.0 environment: sdk: ">=2.14.0 <3.0.0" 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 b903678ecef8..cc29fc755067 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 @@ -14,6 +14,7 @@ import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart'; @GenerateMocks([ + CookieManagerHostApi, DownloadListener, JavaScriptChannel, TestDownloadListenerHostApi, @@ -647,4 +648,20 @@ void main() { }); }); }); + + group('CookieManager', () { + test('setCookie calls setCookie on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + CookieManager.instance.setCookie('foo', 'bar'); + verify(CookieManager.api.setCookie('foo', 'bar')); + }); + + test('clearCookies calls clearCookies on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + when(CookieManager.api.clearCookies()) + .thenAnswer((_) => Future.value(true)); + CookieManager.instance.clearCookies(); + verify(CookieManager.api.clearCookies()); + }); + }); } 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 8bb569497b59..d25d2338886b 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 @@ -6,14 +6,15 @@ // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. -import 'dart:async' as _i5; -import 'dart:typed_data' as _i4; -import 'dart:ui' as _i6; +import 'dart:async' as _i4; +import 'dart:typed_data' as _i6; +import 'dart:ui' as _i7; 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 _i3; +import 'android_webview.pigeon.dart' as _i5; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -26,6 +27,28 @@ import 'android_webview.pigeon.dart' as _i3; class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} +/// A class which mocks [CookieManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManagerHostApi extends _i1.Mock + implements _i3.CookieManagerHostApi { + MockCookieManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future setCookie(String? arg_url, String? arg_value) => + (super.noSuchMethod(Invocation.method(#setCookie, [arg_url, arg_value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + String toString() => super.toString(); +} + /// A class which mocks [DownloadListener]. /// /// See the documentation for Mockito's code generation for more information. @@ -69,7 +92,7 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { /// /// See the documentation for Mockito's code generation for more information. class MockTestDownloadListenerHostApi extends _i1.Mock - implements _i3.TestDownloadListenerHostApi { + implements _i5.TestDownloadListenerHostApi { MockTestDownloadListenerHostApi() { _i1.throwOnMissingStub(this); } @@ -86,7 +109,7 @@ class MockTestDownloadListenerHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestJavaScriptChannelHostApi extends _i1.Mock - implements _i3.TestJavaScriptChannelHostApi { + implements _i5.TestJavaScriptChannelHostApi { MockTestJavaScriptChannelHostApi() { _i1.throwOnMissingStub(this); } @@ -103,7 +126,7 @@ class MockTestJavaScriptChannelHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebChromeClientHostApi extends _i1.Mock - implements _i3.TestWebChromeClientHostApi { + implements _i5.TestWebChromeClientHostApi { MockTestWebChromeClientHostApi() { _i1.throwOnMissingStub(this); } @@ -121,7 +144,7 @@ class MockTestWebChromeClientHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebSettingsHostApi extends _i1.Mock - implements _i3.TestWebSettingsHostApi { + implements _i5.TestWebSettingsHostApi { MockTestWebSettingsHostApi() { _i1.throwOnMissingStub(this); } @@ -195,7 +218,7 @@ class MockTestWebSettingsHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewClientHostApi extends _i1.Mock - implements _i3.TestWebViewClientHostApi { + implements _i5.TestWebViewClientHostApi { MockTestWebViewClientHostApi() { _i1.throwOnMissingStub(this); } @@ -213,7 +236,7 @@ class MockTestWebViewClientHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewHostApi extends _i1.Mock - implements _i3.TestWebViewHostApi { + implements _i5.TestWebViewHostApi { MockTestWebViewHostApi() { _i1.throwOnMissingStub(this); } @@ -246,7 +269,7 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#loadUrl, [instanceId, url, headers]), returnValueForMissingStub: null); @override - void postUrl(int? instanceId, String? url, _i4.Uint8List? data) => + void postUrl(int? instanceId, String? url, _i6.Uint8List? data) => super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), returnValueForMissingStub: null); @override @@ -279,12 +302,12 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#clearCache, [instanceId, includeDiskFiles]), returnValueForMissingStub: null); @override - _i5.Future evaluateJavascript( + _i4.Future evaluateJavascript( int? instanceId, String? javascriptString) => (super.noSuchMethod( Invocation.method( #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value('')) as _i5.Future); + returnValue: Future.value('')) as _i4.Future); @override String getTitle(int? instanceId) => (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), @@ -353,7 +376,7 @@ class MockTestWebViewHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestAssetManagerHostApi extends _i1.Mock - implements _i3.TestAssetManagerHostApi { + implements _i5.TestAssetManagerHostApi { MockTestAssetManagerHostApi() { _i1.throwOnMissingStub(this); } @@ -403,15 +426,15 @@ class MockWebView extends _i1.Mock implements _i2.WebView { (super.noSuchMethod(Invocation.getter(#settings), returnValue: _FakeWebSettings_0()) as _i2.WebSettings); @override - _i5.Future loadData( + _i4.Future loadData( {String? data, String? mimeType, String? encoding}) => (super.noSuchMethod( Invocation.method(#loadData, [], {#data: data, #mimeType: mimeType, #encoding: encoding}), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future loadDataWithBaseUrl( + _i4.Future loadDataWithBaseUrl( {String? baseUrl, String? data, String? mimeType, @@ -426,114 +449,114 @@ class MockWebView extends _i1.Mock implements _i2.WebView { #historyUrl: historyUrl }), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future loadUrl(String? url, Map? headers) => + _i4.Future loadUrl(String? url, Map? headers) => (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future postUrl(String? url, _i4.Uint8List? data) => + _i4.Future postUrl(String? url, _i6.Uint8List? data) => (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future getUrl() => + _i4.Future getUrl() => (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future canGoBack() => + _i4.Future canGoBack() => (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); + returnValue: Future.value(false)) as _i4.Future); @override - _i5.Future canGoForward() => + _i4.Future canGoForward() => (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); + returnValue: Future.value(false)) as _i4.Future); @override - _i5.Future goBack() => + _i4.Future goBack() => (super.noSuchMethod(Invocation.method(#goBack, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future goForward() => + _i4.Future goForward() => (super.noSuchMethod(Invocation.method(#goForward, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future reload() => + _i4.Future reload() => (super.noSuchMethod(Invocation.method(#reload, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future clearCache(bool? includeDiskFiles) => + _i4.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future evaluateJavascript(String? javascriptString) => (super + _i4.Future evaluateJavascript(String? javascriptString) => (super .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future getTitle() => + _i4.Future getTitle() => (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future scrollTo(int? x, int? y) => + _i4.Future scrollTo(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future scrollBy(int? x, int? y) => + _i4.Future scrollBy(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future getScrollX() => + _i4.Future getScrollX() => (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i5.Future); + returnValue: Future.value(0)) as _i4.Future); @override - _i5.Future getScrollY() => + _i4.Future getScrollY() => (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i5.Future); + returnValue: Future.value(0)) as _i4.Future); @override - _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future addJavaScriptChannel( + _i4.Future addJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future removeJavaScriptChannel( + _i4.Future removeJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setDownloadListener(_i2.DownloadListener? listener) => + _i4.Future setDownloadListener(_i2.DownloadListener? listener) => (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => + _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setBackgroundColor(_i6.Color? color) => + _i4.Future setBackgroundColor(_i7.Color? color) => (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future release() => + _i4.Future release() => (super.noSuchMethod(Invocation.method(#release, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override String toString() => super.toString(); } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart new file mode 100644 index 000000000000..4f274ff4499f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_android_cookie_manager_test.mocks.dart'; + +@GenerateMocks([android_webview.CookieManager]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + android_webview.CookieManager.instance = MockCookieManager(); + }); + + test('clearCookies should call android_webview.clearCookies', () { + when(android_webview.CookieManager.instance.clearCookies()) + .thenAnswer((_) => Future.value(true)); + WebViewAndroidCookieManager().clearCookies(); + verify(android_webview.CookieManager.instance.clearCookies()); + }); + + test('setCookie should throw ArgumentError for cookie with invalid path', () { + expect( + () => WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'flutter.dev', + path: 'invalid;path', + )), + throwsA(const TypeMatcher()), + ); + }); + + test( + 'setCookie should call android_webview.csetCookie with properly formatted cookie value', + () { + WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo&', + value: 'bar@', + domain: 'flutter.dev', + )); + verify(android_webview.CookieManager.instance + .setCookie('flutter.dev', 'foo%26=bar%40; path=/')); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..131977bd3dfd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/webview_android_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_android/src/android_webview.dart' as _i2; + +// 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 [CookieManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManager extends _i1.Mock implements _i2.CookieManager { + MockCookieManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(String? url, String? value) => + (super.noSuchMethod(Invocation.method(#setCookie, [url, value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i3.Future); + @override + String toString() => super.toString(); +} From ab99ed5d590ecf25ea606a6fc32aba5569b09e69 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 7 Dec 2021 20:24:05 +0100 Subject: [PATCH 228/259] [webview_flutter] Add iOS implementations for new cookie manager, to allow setting cookies directly and on webview creation. (#4556) --- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../webview_flutter_test.dart | 11 +- .../ios/Runner.xcodeproj/project.pbxproj | 73 +++++----- .../ios/RunnerTests/FLTCookieManagerTests.m | 127 ++++++++++++++++++ .../FLTWKNavigationDelegateTests.m | 1 + .../example/ios/RunnerTests/FLTWebViewTests.m | 26 +++- .../example/lib/main.dart | 24 +++- .../example/lib/web_view.dart | 24 ++++ .../ios/Classes/FLTCookieManager.h | 6 + .../ios/Classes/FLTCookieManager.m | 62 ++++++++- .../ios/Classes/FLTCookieManager_Test.h | 20 +++ .../ios/Classes/FLTWebViewFlutterPlugin.m | 5 +- .../ios/Classes/FlutterWebView.h | 4 +- .../ios/Classes/FlutterWebView.m | 9 +- .../ios/Classes/FlutterWebView.modulemap | 1 + .../lib/src/webview_cupertino.dart | 8 +- .../lib/src/wkwebview_cookie_manager.dart | 30 +++++ .../lib/webview_flutter_wkwebview.dart | 1 + .../webview_flutter_wkwebview/pubspec.yaml | 4 +- .../src/wkwebview_cookie_manager_test.dart | 65 +++++++++ 20 files changed, 448 insertions(+), 57 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 22f45f27c4d7..c1d27121f9ee 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + ## 2.5.0 * Adds an option to set the background color of the webview. 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 8806495e629c..1e4adb9f7de0 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 @@ -1161,16 +1161,7 @@ String _webviewBool(bool 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;'); -} - -Future _runJavascriptReturningResult( - WebViewController controller, String js) async { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.runJavascriptReturningResult(js); - } - return jsonDecode(await controller.runJavascriptReturningResult(js)) - as String; + return await controller.runJavascriptReturningResult('navigator.userAgent;'); } class ResizableWebView extends StatefulWidget { 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 e292b1bd52fa..b681c4704ddd 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 @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -16,8 +16,9 @@ 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 */; }; - AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */; }; + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */; }; DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 572FFC2B2BA326B420B22679 /* libPods-Runner.a */; }; + E43693B527512C0F00382F85 /* FLTCookieManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -54,10 +55,11 @@ /* 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 = ""; }; + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 572FFC2B2BA326B420B22679 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -74,7 +76,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCookieManagerTests.m; sourceTree = ""; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -86,7 +88,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */, + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,7 +114,7 @@ isa = PBXGroup; children = ( 572FFC2B2BA326B420B22679 /* libPods-Runner.a */, - 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */, + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -123,6 +125,7 @@ 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + E43693B427512C0F00382F85 /* FLTCookieManagerTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -190,8 +193,8 @@ children = ( F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */, B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */, - C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, - 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, + 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */, + 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -212,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */, + AA38EF430495C2FB50F0F114 /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -277,13 +280,16 @@ ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 68BDCAE823C3F7CB00D9C032 = { + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; F7151F73266057800028CB91 = { CreatedOnToolsVersion = 12.5; + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -338,7 +344,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */ = { + 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"; + }; + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -353,28 +373,28 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */ = { + AA38EF430495C2FB50F0F114 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -389,27 +409,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -419,6 +425,7 @@ files = ( 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + E43693B527512C0F00382F85 /* FLTCookieManagerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -477,7 +484,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -491,7 +498,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m new file mode 100644 index 000000000000..837c0d892b64 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTCookieManagerTests.m @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTCookieManagerTests : XCTestCase + +@end + +@implementation FLTCookieManagerTests + +- (void)setUp { + [super setUp]; +} + +- (void)testSetCookieForResultSetsCookieAndReturnsResultOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + XCTestExpectation *resultExpectation = [self + expectationWithDescription:@"Should return success result when setting cookie completes."]; + [FLTCookieManager.instance setHttpCookieStore:OCMClassMock(WKHTTPCookieStore.class)]; + NSDictionary *arguments = @{ + @"name" : @"foo", + @"value" : @"bar", + @"domain" : @"flutter.dev", + @"path" : @"/", + }; + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : arguments[@"name"], + NSHTTPCookieValue : arguments[@"value"], + NSHTTPCookieDomain : arguments[@"domain"], + NSHTTPCookiePath : arguments[@"path"], + }]; + [OCMStub([FLTCookieManager.instance.httpCookieStore setCookie:[OCMArg isEqual:cookie] + completionHandler:[OCMArg any]]) + andDo:^(NSInvocation *invocation) { + void (^setCookieCompletionHandler)(void); + [invocation getArgument:&setCookieCompletionHandler atIndex:3]; + setCookieCompletionHandler(); + }]; + // Run + [[FLTCookieManager instance] + setCookieForResult:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + } + arguments:arguments]; + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + } +} + +- (void)testSetCookieForDataSetsCookieOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + WKHTTPCookieStore *mockHttpCookieStore = OCMClassMock(WKHTTPCookieStore.class); + [FLTCookieManager.instance setHttpCookieStore:mockHttpCookieStore]; + NSDictionary *cookieData = @{ + @"name" : @"foo", + @"value" : @"bar", + @"domain" : @"flutter.dev", + @"path" : @"/", + }; + // Run + [[FLTCookieManager instance] setCookieForData:cookieData]; + // Verify + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieData[@"name"], + NSHTTPCookieValue : cookieData[@"value"], + NSHTTPCookieDomain : cookieData[@"domain"], + NSHTTPCookiePath : cookieData[@"path"], + }]; + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie] + completionHandler:[OCMArg any]]); + } +} + +- (void)testSetCookiesForDataSetsCookiesOnIOS11 { + if (@available(iOS 11.0, *)) { + // Setup + WKHTTPCookieStore *mockHttpCookieStore = OCMClassMock(WKHTTPCookieStore.class); + [FLTCookieManager.instance setHttpCookieStore:mockHttpCookieStore]; + NSArray *cookieDatas = @[ + @{ + @"name" : @"foo1", + @"value" : @"bar1", + @"domain" : @"flutter.dev", + @"path" : @"/", + }, + @{ + @"name" : @"foo2", + @"value" : @"bar2", + @"domain" : @"flutter2.dev", + @"path" : @"/2", + } + ]; + // Run + [[FLTCookieManager instance] setCookiesForData:cookieDatas]; + // Verify + NSHTTPCookie *cookie1 = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieDatas[0][@"name"], + NSHTTPCookieValue : cookieDatas[0][@"value"], + NSHTTPCookieDomain : cookieDatas[0][@"domain"], + NSHTTPCookiePath : cookieDatas[0][@"path"], + }]; + + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie1] + completionHandler:[OCMArg any]]); + NSHTTPCookie *cookie2 = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieDatas[1][@"name"], + NSHTTPCookieValue : cookieDatas[1][@"value"], + NSHTTPCookieDomain : cookieDatas[1][@"domain"], + NSHTTPCookiePath : cookieDatas[1][@"path"], + }]; + OCMVerify([mockHttpCookieStore setCookie:[OCMArg isEqual:cookie2] + completionHandler:[OCMArg any]]); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m index a819a9b53d60..d39a9f203d70 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -5,6 +5,7 @@ @import Flutter; @import XCTest; @import webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; // OCMock library doesn't generate a valid modulemap. #import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index a3c314a79d2a..6c7c6bffbc3f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -16,6 +16,8 @@ @interface FLTWebViewTests : XCTestCase @property(strong, nonatomic) NSObject *mockBinaryMessenger; +@property(strong, nonatomic) FLTCookieManager *mockCookieManager; + @end @implementation FLTWebViewTests @@ -23,6 +25,7 @@ @implementation FLTWebViewTests - (void)setUp { [super setUp]; self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockCookieManager = OCMClassMock(FLTCookieManager.class); } - (void)testCanInitFLTWebViewController { @@ -35,8 +38,8 @@ - (void)testCanInitFLTWebViewController { } - (void)testCanInitFLTWebViewFactory { - FLTWebViewFactory *factory = - [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + FLTWebViewFactory *factory = [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger + cookieManager:self.mockCookieManager]; XCTAssertNotNil(factory); } @@ -648,7 +651,7 @@ - (void)testOnLoadUrlLoadsRequestWithSuccessResult { [self waitForExpectationsWithTimeout:30.0 handler:nil]; } -- (void)testOnLoadRequestReturnsErroResultForInvalidRequest { +- (void)testOnLoadRequestReturnsErrorResultForInvalidRequest { // Setup FLTWebViewController *controller = [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) @@ -697,4 +700,21 @@ - (void)testOnLoadRequestLoadsRequestWithSuccessResult { [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (void)testCreateWithFrameShouldSetCookiesOnIOS11 { + if (@available(iOS 11, *)) { + // Setup + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger + cookieManager:self.mockCookieManager]; + NSArray *cookies = + @[ @{@"name" : @"foo", @"value" : @"bar", @"domain" : @"flutter.dev", @"path" : @"/"} ]; + // Run + [factory createWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:@{@"cookies" : cookies}]; + // Verify + OCMVerify([_mockCookieManager setCookiesForData:cookies]); + } +} + @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 41297bfd94fe..ce7548a87cab 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -88,6 +88,11 @@ class _WebViewExampleState extends State<_WebViewExample> { final Completer _controller = Completer(); + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -104,7 +109,7 @@ class _WebViewExampleState extends State<_WebViewExample> { // to allow calling Scaffold.of(context) so we can show a snackbar. body: Builder(builder: (BuildContext context) { return WebView( - initialUrl: 'https://flutter.dev', + initialUrl: 'https://flutter.dev/', onWebViewCreated: (WebViewController controller) { _controller.complete(controller); }, @@ -169,6 +174,7 @@ enum _MenuOptions { loadLocalFile, loadHtmlString, doPostRequest, + setCookie, transparentBackground, } @@ -217,6 +223,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.doPostRequest: _onDoPostRequest(controller.data!, context); break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; case _MenuOptions.transparentBackground: _onTransparentBackground(controller.data!, context); break; @@ -264,6 +273,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.doPostRequest, child: Text('Post Request'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), const PopupMenuItem<_MenuOptions>( key: ValueKey('ShowTransparentBackgroundExample'), value: _MenuOptions.transparentBackground, @@ -365,6 +378,15 @@ class _SampleMenu extends StatelessWidget { await controller.loadRequest(request); } + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index 69fa2bdb79e3..1efd0315072b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -54,6 +55,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -95,6 +97,9 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -285,6 +290,7 @@ class _WebViewState extends State { _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + cookies: widget.initialCookies, backgroundColor: widget.backgroundColor, ), javascriptChannelRegistry: _javascriptChannelRegistry, @@ -660,3 +666,21 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_wkwebview.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h index 8fe331875250..dc5b8f589ade 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h @@ -9,6 +9,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTCookieManager : NSObject ++ (FLTCookieManager*)instance; + +- (void)setCookiesForData:(NSArray*)cookies; + +- (void)setCookieForData:(NSDictionary*)cookie; + @end NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m index f4783ffb4123..39976d11153e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m @@ -3,22 +3,32 @@ // found in the LICENSE file. #import "FLTCookieManager.h" +#import "FLTCookieManager_Test.h" @implementation FLTCookieManager { + WKHTTPCookieStore *_httpCookieStore API_AVAILABLE(macos(10.13), ios(11.0)); } -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTCookieManager *instance = [[FLTCookieManager alloc] init]; ++ (FLTCookieManager *)instance { + static FLTCookieManager *instance = nil; + if (instance == nil) { + instance = [[FLTCookieManager alloc] init]; + } + return instance; +} ++ (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addMethodCallDelegate:[self instance] channel:channel]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([[call method] isEqualToString:@"clearCookies"]) { [self clearCookies:result]; + } else if ([[call method] isEqualToString:@"setCookie"]) { + [self setCookieForResult:result arguments:[call arguments]]; } else { result(FlutterMethodNotImplemented); } @@ -46,4 +56,50 @@ - (void)clearCookies:(FlutterResult)result { } } +- (void)setCookiesForData:(NSArray *)cookies { + for (id cookie in cookies) { + [self setCookieForData:cookie]; + } +} + +- (void)setCookieForData:(NSDictionary *)cookieData { + if (@available(iOS 11.0, *)) { + if (!_httpCookieStore) { + _httpCookieStore = [[WKWebsiteDataStore defaultDataStore] httpCookieStore]; + } + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : cookieData[@"name"], + NSHTTPCookieValue : cookieData[@"value"], + NSHTTPCookieDomain : cookieData[@"domain"], + NSHTTPCookiePath : cookieData[@"path"], + }]; + [_httpCookieStore setCookie:cookie + completionHandler:^{ + }]; + } else { + NSLog(@"Setting cookies is not supported for Flutter WebViews prior to iOS 11."); + } +} + +- (void)setCookieForResult:(FlutterResult)result arguments:(NSDictionary *)arguments { + if (@available(iOS 11.0, *)) { + if (!_httpCookieStore) { + _httpCookieStore = [[WKWebsiteDataStore defaultDataStore] httpCookieStore]; + } + NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookieName : arguments[@"name"], + NSHTTPCookieValue : arguments[@"value"], + NSHTTPCookieDomain : arguments[@"domain"], + NSHTTPCookiePath : arguments[@"path"], + }]; + [_httpCookieStore setCookie:cookie + completionHandler:^{ + result(nil); + }]; + } else { + NSLog(@"Setting cookies is not supported for Flutter WebViews prior to iOS 11."); + result(nil); + } +} + @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h new file mode 100644 index 000000000000..fecec4932570 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager_Test.h @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import webview_flutter_wkwebview.Test;" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTCookieManager () + +@property(nonatomic, strong) + WKHTTPCookieStore *httpCookieStore API_AVAILABLE(macos(10.13), ios(11.0)); + +- (void)setCookieForResult:(FlutterResult)result arguments:(NSDictionary *)arguments; + +@end + +NS_ASSUME_NONNULL_END 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 9f01416acc6a..0d04d6c3a9ee 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -9,10 +9,11 @@ @implementation FLTWebViewFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { + [FLTCookieManager registerWithRegistrar:registrar]; FLTWebViewFactory* webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; + [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger + cookieManager:[FLTCookieManager instance]]; [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; - [FLTCookieManager registerWithRegistrar:registrar]; } @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 6d8e463c7b13..145a772c5015 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import #import #import @@ -31,7 +32,8 @@ NS_ASSUME_NONNULL_BEGIN @end @interface FLTWebViewFactory : NSObject -- (instancetype)initWithMessenger:(NSObject*)messenger; +- (instancetype)initWithMessenger:(NSObject*)messenger + cookieManager:(FLTCookieManager*)cookieManager; @end NS_ASSUME_NONNULL_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 25a93a67addc..ea45a8b9b534 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -10,12 +10,15 @@ @implementation FLTWebViewFactory { NSObject* _messenger; + FLTCookieManager* _cookieManager; } -- (instancetype)initWithMessenger:(NSObject*)messenger { +- (instancetype)initWithMessenger:(NSObject*)messenger + cookieManager:(FLTCookieManager*)cookieManager { self = [super init]; if (self) { _messenger = messenger; + _cookieManager = cookieManager; } return self; } @@ -27,6 +30,10 @@ - (instancetype)initWithMessenger:(NSObject*)messenger { - (NSObject*)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { + if (@available(iOS 11.0, *)) { + [_cookieManager setCookiesForData:args[@"cookies"]]; + } + FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame viewIdentifier:viewId arguments:args diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap index fa5143060381..096507557688 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap @@ -6,5 +6,6 @@ framework module webview_flutter_wkwebview { explicit module Test { header "FlutterWebView_Test.h" + header "FLTCookieManager_Test.h" } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart index 05b79d0a72e4..cd00d9a7d14b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart @@ -45,5 +45,11 @@ class CupertinoWebView implements WebViewPlatform { } @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart new file mode 100644 index 000000000000..d460ce02ab68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WKWebViewCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + + @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 MethodChannelWebViewPlatform.setCookie(cookie); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart index bbec415dccd0..f647ab38a41b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -3,3 +3,4 @@ // found in the LICENSE file. export 'src/webview_cupertino.dart'; +export 'src/wkwebview_cookie_manager.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 702473382793..fc274b453995 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.5.0 +version: 2.6.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.7.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: flutter_driver: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart new file mode 100644 index 000000000000..54b1921583b9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/wkwebview_cookie_manager.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const MethodChannel cookieChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + final List log = []; + + cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + if (methodCall.method == 'clearCookies') { + return true; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('clearCookies should call `clearCookies` on the method channel', + () async { + await WKWebViewCookieManager().clearCookies(); + expect( + log, + [ + isMethodCall( + 'clearCookies', + arguments: null, + ), + ], + ); + }); + + test('setCookie should call `setCookie` on the method channel', () async { + await WKWebViewCookieManager().setCookie( + const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'), + ); + expect( + log, + [ + isMethodCall( + 'setCookie', + arguments: { + 'name': 'foo', + 'value': 'bar', + 'domain': 'flutter.dev', + 'path': '/', + }, + ), + ], + ); + }); +} From 131036d0282dee7b0ee3c2bb4279035ac77145fe Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 7 Dec 2021 21:39:05 +0100 Subject: [PATCH 229/259] [webview_flutter] WKWebView implementation of loadFlutterAsset method. (#4582) --- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../example/assets/www/index.html | 20 +++ .../example/assets/www/styles/style.css | 3 + .../example/ios/RunnerTests/FLTWebViewTests.m | 117 +++++++++++++++++- .../example/lib/main.dart | 13 ++ .../example/lib/web_view.dart | 8 ++ .../example/pubspec.yaml | 2 + .../ios/Classes/FlutterWebView.m | 30 +++++ .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 9 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index c1d27121f9ee..0aaf4bcf57aa 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + ## 2.6.0 * Implements new cookie manager for setting cookies and providing initial cookies. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m index 6c7c6bffbc3f..976b9c46579a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -164,7 +164,120 @@ - (void)testLoadFileFailsWithInvalidPath { OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); } -- (void)testLoadFileSucceedsWithBaseUrl { +- (void)testLoadFlutterAssetSucceeds { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"]; + NSURL *url = [NSURL URLWithString:[@"file:///" stringByAppendingString:filePath]]; + [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension] + withExtension:@"html"]) andReturn:(url)]; + + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@"assets/file.html"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + [self waitForExpectations:@[ resultExpectation ] timeout:1.0]; + OCMVerify([mockWebView loadFileURL:url + allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]); +} + +- (void)testLoadFlutterAssetFailsWithInvalidKey { + NSArray *resultExpectations = @[ + [self expectationWithDescription:@"Should return failed result when argument is nil."], + [self expectationWithDescription: + @"Should return failed result when argument is not of type NSString*."], + [self expectationWithDescription: + @"Should return failed result when argument is an empty string."], + ]; + + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:nil] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument is nil."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[0] fulfill]; + }]; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@(10)] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument is not of type NSString."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[1] fulfill]; + }]; + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@""] + result:^(id _Nullable result) { + FlutterError *expected = + [FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:@"Argument contains an empty string."]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectations[2] fulfill]; + }]; + + [self waitForExpectations:resultExpectations timeout:1.0]; + OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); +} + +- (void)testLoadFlutterAssetFailsWithParsingError { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + NSString *filePath = [FlutterDartProject lookupKeyForAsset:@"assets/file.html"]; + [OCMStub([mockBundle URLForResource:[filePath stringByDeletingPathExtension] + withExtension:@"html"]) andReturn:(nil)]; + + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return failed result over the method channel."]; + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class); + controller.webView = mockWebView; + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFlutterAsset" + arguments:@"assets/file.html"] + result:^(id _Nullable result) { + FlutterError *expected = [FlutterError + errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Failed parsing file path for supplied key." + details:[NSString + stringWithFormat: + @"Failed to convert path '%@' into NSURL for key '%@'.", + filePath, @"assets/file.html"]]; + [FLTWebViewTests assertFlutterError:result withExpected:expected]; + [resultExpectation fulfill]; + }]; + + [self waitForExpectations:@[ resultExpectation ] timeout:1.0]; + OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]); +} + +- (void)testLoadHtmlStringSucceedsWithBaseUrl { NSURL *baseUrl = [NSURL URLWithString:@"https://flutter.dev"]; XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return successful result over the method channel."]; @@ -189,7 +302,7 @@ - (void)testLoadFileSucceedsWithBaseUrl { OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:baseUrl]); } -- (void)testLoadFileSucceedsWithoutBaseUrl { +- (void)testLoadHtmlStringSucceedsWithoutBaseUrl { XCTestExpectation *resultExpectation = [self expectationWithDescription:@"Should return successful result over the method channel."]; FLTWebViewController *controller = 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 ce7548a87cab..d4e0ec4aba0c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -171,6 +171,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadFlutterAsset, loadLocalFile, loadHtmlString, doPostRequest, @@ -214,6 +215,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case _MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; @@ -261,6 +265,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), const PopupMenuItem<_MenuOptions>( value: _MenuOptions.loadHtmlString, child: Text('Load HTML string'), @@ -355,6 +363,11 @@ class _SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + Future _onLoadLocalFileExample( WebViewController controller, BuildContext context) async { final String pathToIndex = await _prepareLocalFile(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index 1efd0315072b..4d479f943d62 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -319,6 +319,14 @@ class WebViewController { WebView _widget; + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the file located on the specified [absoluteFilePath]. /// /// The [absoluteFilePath] parameter should contain the absolute path to the diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 2070b1161ba0..b8c2464eb051 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -34,3 +34,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css 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 ea45a8b9b534..3f3b9d917783 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -163,6 +163,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onUpdateSettings:call result:result]; } else if ([[call method] isEqualToString:@"loadFile"]) { [self onLoadFile:call result:result]; + } else if ([[call method] isEqualToString:@"loadFlutterAsset"]) { + [self onLoadFlutterAsset:call result:result]; } else if ([[call method] isEqualToString:@"loadHtmlString"]) { [self onLoadHtmlString:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { @@ -244,6 +246,34 @@ - (void)onLoadFile:(FlutterMethodCall*)call result:(FlutterResult)result { result(nil); } +- (void)onLoadFlutterAsset:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* error = nil; + if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) { + result([FlutterError errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Supplied asset key is not valid." + details:error]); + return; + } + + NSString* assetKey = [call arguments]; + NSString* assetFilePath = [FlutterDartProject lookupKeyForAsset:assetKey]; + NSURL* url = [[NSBundle mainBundle] URLForResource:[assetFilePath stringByDeletingPathExtension] + withExtension:assetFilePath.pathExtension]; + + if (!url) { + result([FlutterError + errorWithCode:@"loadFlutterAsset_invalidKey" + message:@"Failed parsing file path for supplied key." + details:[NSString + stringWithFormat:@"Failed to convert path '%@' into NSURL for key '%@'.", + assetFilePath, assetKey]]); + return; + } + + [_webView loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]; + result(nil); +} + - (void)onLoadHtmlString:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary* arguments = [call arguments]; if (![arguments isKindOfClass:NSDictionary.class]) { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index fc274b453995..08e98b11d4bf 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.6.0 +version: 2.7.0 environment: sdk: ">=2.14.0 <3.0.0" From 4fdf85cb9f1a6d52525a611fdb4f0b722bc106cd Mon Sep 17 00:00:00 2001 From: BeMacized Date: Tue, 7 Dec 2021 21:53:00 +0100 Subject: [PATCH 230/259] [webview_flutter] Add setCookie to CookieManager and support initial cookies in creation params. (#4561) This PR adds a function for setting cookies to the CookieManager, as well support for setting initial cookies through the `CreationParams` object. Fixes flutter/flutter#27597 --- .../webview_flutter/CHANGELOG.md | 5 +++ .../webview_flutter/example/lib/main.dart | 17 ++++++++ .../lib/platform_interface.dart | 1 + .../webview_flutter/lib/src/webview.dart | 32 +++++++++++++-- .../webview_flutter/pubspec.yaml | 6 +-- .../test/webview_flutter_test.dart | 41 +++++++++++++++++-- 6 files changed, 92 insertions(+), 10 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index a9e133d08d47..1df7e53d8e92 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.7.0 + +* Adds `setCookie` to CookieManager. +* CreationParams now supports setting `initialCookies`. + ## 2.6.0 * Adds support for the `loadRequest` method. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 02e6ef85ec61..65786de1f8b9 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -181,6 +181,7 @@ enum MenuOptions { loadLocalFile, loadHtmlString, transparentBackground, + setCookie, } class SampleMenu extends StatelessWidget { @@ -232,6 +233,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.transparentBackground: _onTransparentBackground(controller.data!, context); break; + case MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -281,6 +285,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), ], ); }, @@ -357,6 +365,15 @@ class SampleMenu extends StatelessWidget { await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await CookieManager().setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Future _onDoPostRequest( WebViewController controller, BuildContext context) async { final WebViewRequest request = WebViewRequest( diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart index ab1cbb1bd344..48f74346fe61 100644 --- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart @@ -22,5 +22,6 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte WebSettings, WebResourceError, WebResourceErrorType, + WebViewCookie, WebViewRequest, WebViewRequestMethod; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index d76f7b359482..8fe4f4147cd9 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -3,12 +3,15 @@ // found in the LICENSE file. import 'dart:async'; +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.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.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'; @@ -79,6 +82,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -150,6 +154,9 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -365,6 +372,7 @@ CreationParams _creationParamsfromWidget(WebView widget) { userAgent: widget.userAgent, autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ); } @@ -779,16 +787,32 @@ class CookieManager { return _instance ??= CookieManager._(); } - CookieManager._(); + CookieManager._() { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported by webview_flutter.'); + } + } + } static CookieManager? _instance; /// Clears all cookies for all [WebView] instances. /// - /// This is a no op on iOS version smaller than 9. - /// /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); + Future clearCookies() => + WebViewCookieManagerPlatform.instance!.clearCookies(); + + /// Sets a cookie for all [WebView] instances. + /// + /// This is a no op on iOS versions below 11. + Future setCookie(WebViewCookie cookie) => + WebViewCookieManagerPlatform.instance!.setCookie(cookie); } // Throws an ArgumentError if `url` is not a valid URL string. diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 1bc6bab8b3ff..cd6c618025fe 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.6.0 +version: 2.7.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.7.0 + webview_flutter_android: ^2.8.0 webview_flutter_platform_interface: ^1.8.0 - webview_flutter_wkwebview: ^2.5.0 + webview_flutter_wkwebview: ^2.6.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index d422402dbae7..40baef7e0ab5 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -23,10 +23,12 @@ void main() { late MockWebViewPlatform mockWebViewPlatform; late MockWebViewPlatformController mockWebViewPlatformController; + late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform; setUp(() { mockWebViewPlatformController = MockWebViewPlatformController(); mockWebViewPlatform = MockWebViewPlatform(); + mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform(); when(mockWebViewPlatform.build( context: anyNamed('context'), creationParams: anyNamed('creationParams'), @@ -46,6 +48,11 @@ void main() { }); WebView.platform = mockWebViewPlatform; + WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform; + }); + + tearDown(() { + mockWebViewCookieManagerPlatform.reset(); }); testWidgets('Create WebView', (WidgetTester tester) async { @@ -499,9 +506,6 @@ void main() { }); testWidgets('Cookies can be cleared once', (WidgetTester tester) async { - when(mockWebViewPlatform.clearCookies()) - .thenAnswer((_) => Future.value(true)); - await tester.pumpWidget( const WebView( initialUrl: 'https://flutter.io', @@ -512,6 +516,21 @@ void main() { expect(hasCookies, true); }); + testWidgets('Cookies can be set', (WidgetTester tester) async { + const WebViewCookie cookie = + WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + await cookieManager.setCookie(cookie); + expect(mockWebViewCookieManagerPlatform.setCookieCalls, + [cookie]); + }); + testWidgets('Initial JavaScript channels', (WidgetTester tester) async { await tester.pumpWidget( WebView( @@ -1308,3 +1327,19 @@ class MatchesCreationParams extends Matcher { .matches(creationParams.javascriptChannelNames, matchState); } } + +class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform { + List setCookieCalls = []; + + @override + Future clearCookies() async => true; + + @override + Future setCookie(WebViewCookie cookie) async { + setCookieCalls.add(cookie); + } + + void reset() { + setCookieCalls = []; + } +} From 7d44b40ee4f8b3e737ba5e913fb0a441c1b0f01c Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 7 Dec 2021 23:24:06 +0100 Subject: [PATCH 231/259] [webview_flutter] Implements the loadFlutterAsset in the app facing package. (#4593) --- .../webview_flutter/CHANGELOG.md | 4 +++ .../example/assets/www/index.html | 20 +++++++++++ .../example/assets/www/styles/style.css | 3 ++ .../webview_flutter/example/lib/main.dart | 13 +++++++ .../webview_flutter/example/pubspec.yaml | 2 ++ .../webview_flutter/lib/src/webview.dart | 9 +++++ .../webview_flutter/pubspec.yaml | 4 +-- .../test/webview_flutter_test.dart | 34 +++++++++++++++++++ 8 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter/example/assets/www/index.html create mode 100644 packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 1df7e53d8e92..4906003a2e02 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.0 + +* Adds support for the `loadFlutterAsset` method. + ## 2.7.0 * Adds `setCookie` to CookieManager. diff --git a/packages/webview_flutter/webview_flutter/example/assets/www/index.html b/packages/webview_flutter/webview_flutter/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 65786de1f8b9..88c240a7316c 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -179,6 +179,7 @@ enum MenuOptions { navigationDelegate, doPostRequest, loadLocalFile, + loadFlutterAsset, loadHtmlString, transparentBackground, setCookie, @@ -227,6 +228,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.loadLocalFile: _onLoadLocalFileExample(controller.data!, context); break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; case MenuOptions.loadHtmlString: _onLoadHtmlStringExample(controller.data!, context); break; @@ -280,6 +284,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.loadLocalFile, child: Text('Load local file'), ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), const PopupMenuItem( key: ValueKey('ShowTransparentBackgroundExample'), value: MenuOptions.transparentBackground, @@ -392,6 +400,11 @@ class SampleMenu extends StatelessWidget { await controller.loadFile(pathToIndex); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + Future _onLoadHtmlStringExample( WebViewController controller, BuildContext context) async { await controller.loadHtmlString(kLocalExamplePage); diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 284a7a9d5a0d..ae3b57e07a89 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -33,3 +33,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 8fe4f4147cd9..7b907a5f8bb2 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -526,6 +526,15 @@ class WebViewController { return _webViewPlatformController.loadFile(absoluteFilePath); } + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + assert(key.isNotEmpty); + return _webViewPlatformController.loadFlutterAsset(key); + } + /// Loads the supplied HTML string. /// /// The [baseUrl] parameter is used when resolving relative URLs within the diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index cd6c618025fe..4ac5e1d8add3 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.7.0 +version: 2.8.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter webview_flutter_android: ^2.8.0 webview_flutter_platform_interface: ^1.8.0 - webview_flutter_wkwebview: ^2.6.0 + webview_flutter_wkwebview: ^2.7.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index 40baef7e0ab5..ad25cadf1dc4 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -131,6 +131,40 @@ void main() { expect(() => controller!.loadFile(''), throwsAssertionError); }); + testWidgets('Load Flutter asset', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadFlutterAsset('assets/index.html'); + + verify(mockWebViewPlatformController.loadFlutterAsset( + 'assets/index.html', + )); + }); + + testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadFlutterAsset(''), throwsAssertionError); + }); + testWidgets('Load HTML string without base URL', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( From 84964325d304c2a153e1dd6dc5e96be31ee93d3c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Dec 2021 19:39:12 -0500 Subject: [PATCH 232/259] [webview_flutter] Proof of concept web implementation (#4594) Initial proof of concept implementation of flutter_webview for the web. --- .../webview_flutter_web/AUTHORS | 8 + .../webview_flutter_web/CHANGELOG.md | 4 + .../webview_flutter_web/LICENSE | 26 + .../webview_flutter_web/README.md | 20 + .../webview_flutter_web/example/.metadata | 8 + .../webview_flutter_web/example/README.md | 8 + .../webview_flutter_test.dart | 72 + .../webview_flutter_web/example/lib/main.dart | 93 ++ .../example/lib/web_view.dart | 385 +++++ .../webview_flutter_web/example/pubspec.yaml | 32 + .../example/test_driver/integration_test.dart | 7 + .../example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../example/web/index.html | 104 ++ .../example/web/manifest.json | 35 + .../lib/shims/dart_ui.dart | 10 + .../lib/shims/dart_ui_fake.dart | 33 + .../lib/shims/dart_ui_real.dart | 5 + .../lib/webview_flutter_web.dart | 278 ++++ .../webview_flutter_web/pubspec.yaml | 32 + .../test/webview_flutter_web_test.dart | 130 ++ .../test/webview_flutter_web_test.mocks.dart | 1285 +++++++++++++++++ 25 files changed, 2575 insertions(+) create mode 100644 packages/webview_flutter/webview_flutter_web/AUTHORS create mode 100644 packages/webview_flutter/webview_flutter_web/CHANGELOG.md create mode 100644 packages/webview_flutter/webview_flutter_web/LICENSE create mode 100644 packages/webview_flutter/webview_flutter_web/README.md create mode 100644 packages/webview_flutter/webview_flutter_web/example/.metadata create mode 100644 packages/webview_flutter/webview_flutter_web/example/README.md create mode 100644 packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter_web/example/lib/main.dart create mode 100644 packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart create mode 100644 packages/webview_flutter/webview_flutter_web/example/pubspec.yaml create mode 100644 packages/webview_flutter/webview_flutter_web/example/test_driver/integration_test.dart create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/favicon.png create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-192.png create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-512.png create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/index.html create mode 100644 packages/webview_flutter/webview_flutter_web/example/web/manifest.json create mode 100644 packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart create mode 100644 packages/webview_flutter/webview_flutter_web/pubspec.yaml create mode 100644 packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart create mode 100644 packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart diff --git a/packages/webview_flutter/webview_flutter_web/AUTHORS b/packages/webview_flutter/webview_flutter_web/AUTHORS new file mode 100644 index 000000000000..05432a7fbf9a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/AUTHORS @@ -0,0 +1,8 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Bodhi Mulders + diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md new file mode 100644 index 000000000000..11cfedaaf7e4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -0,0 +1,4 @@ +## 0.1.0 + +* First web implementation for webview_flutter + diff --git a/packages/webview_flutter/webview_flutter_web/LICENSE b/packages/webview_flutter/webview_flutter_web/LICENSE new file mode 100644 index 000000000000..77130909e474 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/LICENSE @@ -0,0 +1,26 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/packages/webview_flutter/webview_flutter_web/README.md b/packages/webview_flutter/webview_flutter_web/README.md new file mode 100644 index 000000000000..9e7e1c6499fe --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/README.md @@ -0,0 +1,20 @@ +# webview\_flutter\_web + +This is an implementation of the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin for web. + +It is currently severely limited and doesn't implement most of the available functionality. +The following functionality is currently available: + +- `loadUrl` (Without headers) +- `requestUrl` +- `loadHTMLString` (Without `baseUrl`) +- Setting the `initialUrl` through `CreationParams`. + +Nothing else is currently supported. + +## Usage + +### Depend on the package + +This package is not an endorsed implementation of the `webview_flutter` plugin yet, so you'll need to +[add it explicitly](https://pub.dev/packages/webview_flutter_web/install). \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_web/example/.metadata b/packages/webview_flutter/webview_flutter_web/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_web/example/README.md b/packages/webview_flutter/webview_flutter_web/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..232ecdd302b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_web_example/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + await controllerCompleter.future; + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, primaryUrl); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, secondaryUrl); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart new file mode 100644 index 000000000000..9cf412d74e50 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'web_view.dart'; + +void main() { + runApp(const MaterialApp(home: _WebViewExample())); +} + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + actions: [ + _SampleMenu(_controller.future), + ], + ), + body: WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + ), + ); + } +} + +enum _MenuOptions { + doPostRequest, +} + +class _SampleMenu extends StatelessWidget { + const _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + ], + ); + }, + ); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } +} 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 new file mode 100644 index 000000000000..787b016d2b77 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart @@ -0,0 +1,385 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.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); + +/// A web view widget for showing html content. +/// +/// The [WebView] widget wraps around the [WebWebViewPlatform]. +/// +/// The [WebView] widget is controlled using the [WebViewController] which is +/// provided through the `onWebViewCreated` callback. +/// +/// In this example project it's main purpose is to facilitate integration +/// testing of the `webview_flutter_web` package. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + }) : super(key: key); + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [WebWebViewPlatform]. + /// This property can be set to use a custom platform implementation for WebViews. + /// Setting `platform` doesn't affect [WebView]s that were already created. + static WebViewPlatform platform = WebWebViewPlatform(); + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// The initial URL to load. + final String? initialUrl; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller.updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + final WebViewController controller = WebViewController( + widget, + webViewPlatformController!, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + ), + javascriptChannelRegistry: + JavascriptChannelRegistry({}), + ); + } +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(); + + @override + FutureOr onNavigationRequest( + {required String url, required bool isForMainFrame}) { + throw UnimplementedError(); + } + + @override + void onPageFinished(String url) {} + + @override + void onPageStarted(String url) {} + + @override + void onProgress(int progress) {} + + @override + void onWebResourceError(WebResourceError error) {} +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + /// Creates a [WebViewController] which can be used to control the provided + /// [WebView] widget. + WebViewController( + this._widget, + this._webViewPlatformController, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + /// Update the widget managed by the [WebViewController]. + Future updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. + /// + /// Returns the evaluation result as a JSON formatted string. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + Future runJavascriptReturningResult(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); + } + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = const WebSetting.absent(); + bool? zoomEnabled; + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + zoomEnabled: zoomEnabled, + ); + } + + // Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: false, + hasProgressTracking: false, + debuggingEnabled: false, + gestureNavigationEnabled: false, + allowsInlineMediaPlayback: true, + userAgent: const WebSetting.of(''), + zoomEnabled: false, + ); +} diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml new file mode 100644 index 000000000000..a98df799e92c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -0,0 +1,32 @@ +name: webview_flutter_web_example +description: Demonstrates how to use the webview_flutter_web plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + webview_flutter_web: + # When depending on this package from a real application you should use: + # webview_flutter_web: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter_web/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/webview_flutter_web/example/web/favicon.png b/packages/webview_flutter/webview_flutter_web/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-192.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-512.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/packages/webview_flutter/webview_flutter_web/example/web/index.html b/packages/webview_flutter/webview_flutter_web/example/web/index.html new file mode 100644 index 000000000000..8b8b5bf92f89 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + webview_flutter_web Example + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_web/example/web/manifest.json b/packages/webview_flutter/webview_flutter_web/example/web/manifest.json new file mode 100644 index 000000000000..1124a93355ec --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "webview_flutter_web Example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart new file mode 100644 index 000000000000..1724dd60eab4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// This file shims dart:ui in web-only scenarios, getting rid of the need to +/// suppress analyzer warnings. + +// TODO(BeMacized): Remove this file once web-only dart:ui APIs, +// are exposed from a dedicated place. flutter/flutter#55000 +export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart new file mode 100644 index 000000000000..8757ca22be17 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.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 'dart:html' as html; + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as available. + +// ignore_for_file: avoid_classes_with_only_static_members +// ignore_for_file: camel_case_types + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static bool registerViewFactory( + String viewTypeId, html.Element Function(int viewId) viewFactory) { + return false; + } +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + /// Shim for getAssetUrl. + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + static String getAssetUrl(String asset) => ''; +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'dart:ui'; diff --git a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart new file mode 100644 index 000000000000..aa427eb6874a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart @@ -0,0 +1,278 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'shims/dart_ui.dart' as ui; + +/// Builds an iframe based WebView. +/// +/// This is used as the default implementation for [WebView.platform] on web. +class WebWebViewPlatform implements WebViewPlatform { + /// Constructs a new instance of [WebWebViewPlatform]. + WebWebViewPlatform() { + ui.platformViewRegistry.registerViewFactory( + 'webview-iframe', + (int viewId) => IFrameElement() + ..id = 'webview-$viewId' + ..width = '100%' + ..height = '100%' + ..style.border = 'none'); + } + + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry? javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return HtmlElementView( + viewType: 'webview-iframe', + onPlatformViewCreated: (int viewId) { + if (onWebViewPlatformCreated == null) { + return; + } + final IFrameElement element = + document.getElementById('webview-$viewId')! as IFrameElement; + if (creationParams.initialUrl != null) { + element.src = creationParams.initialUrl; + } + onWebViewPlatformCreated(WebWebViewPlatformController( + element, + )); + }, + ); + } + + @override + Future clearCookies() async => false; + + /// Gets called when the plugin is registered. + static void registerWith(Registrar registrar) {} +} + +/// Implementation of [WebViewPlatformController] for web. +class WebWebViewPlatformController implements WebViewPlatformController { + /// Constructs a [WebWebViewPlatformController]. + WebWebViewPlatformController(this._element); + + final IFrameElement _element; + HttpRequestFactory _httpRequestFactory = HttpRequestFactory(); + + /// Setter for setting the HttpRequestFactory, for testing purposes. + @visibleForTesting + set httpRequestFactory(HttpRequestFactory factory) { + _httpRequestFactory = factory; + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future canGoBack() { + throw UnimplementedError(); + } + + @override + Future canGoForward() { + throw UnimplementedError(); + } + + @override + Future clearCache() { + throw UnimplementedError(); + } + + @override + Future currentUrl() { + throw UnimplementedError(); + } + + @override + Future evaluateJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future getScrollX() { + throw UnimplementedError(); + } + + @override + Future getScrollY() { + throw UnimplementedError(); + } + + @override + Future getTitle() { + throw UnimplementedError(); + } + + @override + Future goBack() { + throw UnimplementedError(); + } + + @override + Future goForward() { + throw UnimplementedError(); + } + + @override + Future loadUrl(String url, Map? headers) async { + _element.src = url; + } + + @override + Future reload() { + throw UnimplementedError(); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future runJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future runJavascriptReturningResult(String javascript) { + throw UnimplementedError(); + } + + @override + Future scrollBy(int x, int y) { + throw UnimplementedError(); + } + + @override + Future scrollTo(int x, int y) { + throw UnimplementedError(); + } + + @override + Future updateSettings(WebSettings setting) { + throw UnimplementedError(); + } + + @override + Future loadFile(String absoluteFilePath) { + throw UnimplementedError(); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) async { + _element.src = 'data:text/html,' + Uri.encodeFull(html); + } + + @override + Future loadRequest(WebViewRequest request) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + final HttpRequest httpReq = await _httpRequestFactory.request( + request.uri.toString(), + method: request.method.serialize(), + requestHeaders: request.headers, + sendData: request.body); + final String contentType = + httpReq.getResponseHeader('content-type') ?? 'text/html'; + _element.src = + 'data:$contentType,' + Uri.encodeFull(httpReq.responseText ?? ''); + } + + @override + Future loadFlutterAsset(String key) { + throw UnimplementedError(); + } +} + +/// Factory class for creating [HttpRequest] instances. +class HttpRequestFactory { + /// Creates and sends a URL request for the specified [url]. + /// + /// By default `request` will perform an HTTP GET request, but a different + /// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the + /// [method] parameter. (See also [HttpRequest.postFormData] for `POST` + /// requests only. + /// + /// The Future is completed when the response is available. + /// + /// If specified, `sendData` will send data in the form of a [ByteBuffer], + /// [Blob], [Document], [String], or [FormData] along with the HttpRequest. + /// + /// If specified, [responseType] sets the desired response format for the + /// request. By default it is [String], but can also be 'arraybuffer', 'blob', + /// 'document', 'json', or 'text'. See also [HttpRequest.responseType] + /// for more information. + /// + /// The [withCredentials] parameter specified that credentials such as a cookie + /// (already) set in the header or + /// [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2) + /// should be specified for the request. Details to keep in mind when using + /// credentials: + /// + /// /// Using credentials is only useful for cross-origin requests. + /// /// The `Access-Control-Allow-Origin` header of `url` cannot contain a wildcard (///). + /// /// The `Access-Control-Allow-Credentials` header of `url` must be set to true. + /// /// If `Access-Control-Expose-Headers` has not been set to true, only a subset of all the response headers will be returned when calling [getAllResponseHeaders]. + /// + /// The following is equivalent to the [getString] sample above: + /// + /// var name = Uri.encodeQueryComponent('John'); + /// var id = Uri.encodeQueryComponent('42'); + /// HttpRequest.request('users.json?name=$name&id=$id') + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Here's an example of submitting an entire form with [FormData]. + /// + /// var myForm = querySelector('form#myForm'); + /// var data = new FormData(myForm); + /// HttpRequest.request('/submit', method: 'POST', sendData: data) + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Note that requests for file:// URIs are only supported by Chrome extensions + /// with appropriate permissions in their manifest. Requests to file:// URIs + /// will also never fail- the Future will always complete successfully, even + /// when the file cannot be found. + /// + /// See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication). + Future request(String url, + {String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void onProgress(ProgressEvent e)?}) { + return HttpRequest.request(url, + method: method, + withCredentials: withCredentials, + responseType: responseType, + mimeType: mimeType, + requestHeaders: requestHeaders, + sendData: sendData, + onProgress: onProgress); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml new file mode 100644 index 000000000000..3b24476283ec --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -0,0 +1,32 @@ +name: webview_flutter_web +description: A Flutter plugin that provides a WebView widget on web. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 0.1.0 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + web: + pluginClass: WebWebViewPlatform + fileName: webview_flutter_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + webview_flutter_platform_interface: ^1.8.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + mockito: ^5.0.0 diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart new file mode 100644 index 000000000000..90e2ea465782 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html'; +import 'dart:typed_data'; + +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_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; +import './webview_flutter_web_test.mocks.dart'; + +@GenerateMocks([ + IFrameElement, + BuildContext, + CreationParams, + WebViewPlatformCallbacksHandler, + HttpRequestFactory, + HttpRequest, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebWebViewPlatform', () { + test('build returns a HtmlElementView', () { + // Setup + final WebWebViewPlatform platform = WebWebViewPlatform(); + // Run + final Widget widget = platform.build( + context: MockBuildContext(), + creationParams: CreationParams(), + webViewPlatformCallbacksHandler: MockWebViewPlatformCallbacksHandler(), + javascriptChannelRegistry: null, + ); + // Verify + expect(widget, isA()); + }); + }); + + group('WebWebViewPlatformController', () { + test('loadUrl sets url on iframe src attribute', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadUrl('test url', null); + // Verify + verify(mockElement.src = 'test url'); + }); + + test('loadHtmlString loads html into iframe', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadHtmlString('test html'); + // Verify + verify(mockElement.src = 'data:text/html,' + Uri.encodeFull('test html')); + }); + + group('loadRequest', () { + test('loadRequest throws ArgumentError on missing scheme', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run & Verify + expect( + () async => await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('flutter.dev'), + method: WebViewRequestMethod.get, + ), + ), + throwsA(const TypeMatcher())); + }); + + test('loadRequest makes request and loads response into iframe', + () async { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/plain'); + when(mockHttpRequest.responseText).thenReturn('test data'); + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + controller.httpRequestFactory = mockHttpRequestFactory; + // Run + await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: {'Foo': 'Bar'}), + ); + // Verify + verify(mockHttpRequestFactory.request( + 'https://flutter.dev', + method: 'post', + requestHeaders: {'Foo': 'Bar'}, + sendData: Uint8List.fromList('test body'.codeUnits), + )); + verify( + mockElement.src = 'data:text/plain,' + Uri.encodeFull('test data')); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart new file mode 100644 index 000000000000..2507cd292142 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart @@ -0,0 +1,1285 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_web/test/webview_flutter_web_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i6; +import 'dart:html' as _i2; +import 'dart:math' as _i3; + +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/widgets.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/types/auto_media_playback_policy.dart' + as _i8; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i7; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + as _i9; +import 'package:webview_flutter_web/webview_flutter_web.dart' as _i10; + +// 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 _FakeCssClassSet_0 extends _i1.Fake implements _i2.CssClassSet {} + +class _FakeRectangle_1 extends _i1.Fake + implements _i3.Rectangle {} + +class _FakeCssRect_2 extends _i1.Fake implements _i2.CssRect {} + +class _FakePoint_3 extends _i1.Fake implements _i3.Point {} + +class _FakeElementEvents_4 extends _i1.Fake implements _i2.ElementEvents {} + +class _FakeCssStyleDeclaration_5 extends _i1.Fake + implements _i2.CssStyleDeclaration {} + +class _FakeElementStream_6 extends _i1.Fake + implements _i2.ElementStream {} + +class _FakeElementList_7 extends _i1.Fake + implements _i2.ElementList {} + +class _FakeScrollState_8 extends _i1.Fake implements _i2.ScrollState {} + +class _FakeAnimation_9 extends _i1.Fake implements _i2.Animation {} + +class _FakeElement_10 extends _i1.Fake implements _i2.Element {} + +class _FakeShadowRoot_11 extends _i1.Fake implements _i2.ShadowRoot {} + +class _FakeDocumentFragment_12 extends _i1.Fake + implements _i2.DocumentFragment {} + +class _FakeNode_13 extends _i1.Fake implements _i2.Node {} + +class _FakeWidget_14 extends _i1.Fake implements _i4.Widget { + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_15 extends _i1.Fake implements _i4.InheritedWidget { + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_16 extends _i1.Fake implements _i5.DiagnosticsNode { + @override + String toString( + {_i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeHttpRequest_17 extends _i1.Fake implements _i2.HttpRequest {} + +class _FakeHttpRequestUpload_18 extends _i1.Fake + implements _i2.HttpRequestUpload {} + +class _FakeEvents_19 extends _i1.Fake implements _i2.Events {} + +/// A class which mocks [IFrameElement]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockIFrameElement extends _i1.Mock implements _i2.IFrameElement { + MockIFrameElement() { + _i1.throwOnMissingStub(this); + } + + @override + set allow(String? value) => + super.noSuchMethod(Invocation.setter(#allow, value), + returnValueForMissingStub: null); + @override + set allowFullscreen(bool? value) => + super.noSuchMethod(Invocation.setter(#allowFullscreen, value), + returnValueForMissingStub: null); + @override + set allowPaymentRequest(bool? value) => + super.noSuchMethod(Invocation.setter(#allowPaymentRequest, value), + returnValueForMissingStub: null); + @override + set csp(String? value) => super.noSuchMethod(Invocation.setter(#csp, value), + returnValueForMissingStub: null); + @override + set height(String? value) => + super.noSuchMethod(Invocation.setter(#height, value), + returnValueForMissingStub: null); + @override + set name(String? value) => super.noSuchMethod(Invocation.setter(#name, value), + returnValueForMissingStub: null); + @override + set referrerPolicy(String? value) => + super.noSuchMethod(Invocation.setter(#referrerPolicy, value), + returnValueForMissingStub: null); + @override + set src(String? value) => super.noSuchMethod(Invocation.setter(#src, value), + returnValueForMissingStub: null); + @override + set srcdoc(String? value) => + super.noSuchMethod(Invocation.setter(#srcdoc, value), + returnValueForMissingStub: null); + @override + set width(String? value) => + super.noSuchMethod(Invocation.setter(#width, value), + returnValueForMissingStub: null); + @override + set nonce(String? value) => + super.noSuchMethod(Invocation.setter(#nonce, value), + returnValueForMissingStub: null); + @override + Map get attributes => + (super.noSuchMethod(Invocation.getter(#attributes), + returnValue: {}) as Map); + @override + set attributes(Map? value) => + super.noSuchMethod(Invocation.setter(#attributes, value), + returnValueForMissingStub: null); + @override + List<_i2.Element> get children => + (super.noSuchMethod(Invocation.getter(#children), + returnValue: <_i2.Element>[]) as List<_i2.Element>); + @override + set children(List<_i2.Element>? value) => + super.noSuchMethod(Invocation.setter(#children, value), + returnValueForMissingStub: null); + @override + _i2.CssClassSet get classes => + (super.noSuchMethod(Invocation.getter(#classes), + returnValue: _FakeCssClassSet_0()) as _i2.CssClassSet); + @override + set classes(Iterable? value) => + super.noSuchMethod(Invocation.setter(#classes, value), + returnValueForMissingStub: null); + @override + Map get dataset => + (super.noSuchMethod(Invocation.getter(#dataset), + returnValue: {}) as Map); + @override + set dataset(Map? value) => + super.noSuchMethod(Invocation.setter(#dataset, value), + returnValueForMissingStub: null); + @override + _i3.Rectangle get client => + (super.noSuchMethod(Invocation.getter(#client), + returnValue: _FakeRectangle_1()) as _i3.Rectangle); + @override + _i3.Rectangle get offset => + (super.noSuchMethod(Invocation.getter(#offset), + returnValue: _FakeRectangle_1()) as _i3.Rectangle); + @override + String get localName => + (super.noSuchMethod(Invocation.getter(#localName), returnValue: '') + as String); + @override + _i2.CssRect get contentEdge => + (super.noSuchMethod(Invocation.getter(#contentEdge), + returnValue: _FakeCssRect_2()) as _i2.CssRect); + @override + _i2.CssRect get paddingEdge => + (super.noSuchMethod(Invocation.getter(#paddingEdge), + returnValue: _FakeCssRect_2()) as _i2.CssRect); + @override + _i2.CssRect get borderEdge => + (super.noSuchMethod(Invocation.getter(#borderEdge), + returnValue: _FakeCssRect_2()) as _i2.CssRect); + @override + _i2.CssRect get marginEdge => + (super.noSuchMethod(Invocation.getter(#marginEdge), + returnValue: _FakeCssRect_2()) as _i2.CssRect); + @override + _i3.Point get documentOffset => + (super.noSuchMethod(Invocation.getter(#documentOffset), + returnValue: _FakePoint_3()) as _i3.Point); + @override + set innerHtml(String? html) => + super.noSuchMethod(Invocation.setter(#innerHtml, html), + returnValueForMissingStub: null); + @override + String get innerText => + (super.noSuchMethod(Invocation.getter(#innerText), returnValue: '') + as String); + @override + set innerText(String? value) => + super.noSuchMethod(Invocation.setter(#innerText, value), + returnValueForMissingStub: null); + @override + _i2.ElementEvents get on => (super.noSuchMethod(Invocation.getter(#on), + returnValue: _FakeElementEvents_4()) as _i2.ElementEvents); + @override + int get offsetHeight => + (super.noSuchMethod(Invocation.getter(#offsetHeight), returnValue: 0) + as int); + @override + int get offsetLeft => + (super.noSuchMethod(Invocation.getter(#offsetLeft), returnValue: 0) + as int); + @override + int get offsetTop => + (super.noSuchMethod(Invocation.getter(#offsetTop), returnValue: 0) + as int); + @override + int get offsetWidth => + (super.noSuchMethod(Invocation.getter(#offsetWidth), returnValue: 0) + as int); + @override + int get scrollHeight => + (super.noSuchMethod(Invocation.getter(#scrollHeight), returnValue: 0) + as int); + @override + int get scrollLeft => + (super.noSuchMethod(Invocation.getter(#scrollLeft), returnValue: 0) + as int); + @override + set scrollLeft(int? value) => + super.noSuchMethod(Invocation.setter(#scrollLeft, value), + returnValueForMissingStub: null); + @override + int get scrollTop => + (super.noSuchMethod(Invocation.getter(#scrollTop), returnValue: 0) + as int); + @override + set scrollTop(int? value) => + super.noSuchMethod(Invocation.setter(#scrollTop, value), + returnValueForMissingStub: null); + @override + int get scrollWidth => + (super.noSuchMethod(Invocation.getter(#scrollWidth), returnValue: 0) + as int); + @override + String get contentEditable => + (super.noSuchMethod(Invocation.getter(#contentEditable), returnValue: '') + as String); + @override + set contentEditable(String? value) => + super.noSuchMethod(Invocation.setter(#contentEditable, value), + returnValueForMissingStub: null); + @override + set dir(String? value) => super.noSuchMethod(Invocation.setter(#dir, value), + returnValueForMissingStub: null); + @override + bool get draggable => + (super.noSuchMethod(Invocation.getter(#draggable), returnValue: false) + as bool); + @override + set draggable(bool? value) => + super.noSuchMethod(Invocation.setter(#draggable, value), + returnValueForMissingStub: null); + @override + bool get hidden => + (super.noSuchMethod(Invocation.getter(#hidden), returnValue: false) + as bool); + @override + set hidden(bool? value) => + super.noSuchMethod(Invocation.setter(#hidden, value), + returnValueForMissingStub: null); + @override + set inert(bool? value) => super.noSuchMethod(Invocation.setter(#inert, value), + returnValueForMissingStub: null); + @override + set inputMode(String? value) => + super.noSuchMethod(Invocation.setter(#inputMode, value), + returnValueForMissingStub: null); + @override + set lang(String? value) => super.noSuchMethod(Invocation.setter(#lang, value), + returnValueForMissingStub: null); + @override + set spellcheck(bool? value) => + super.noSuchMethod(Invocation.setter(#spellcheck, value), + returnValueForMissingStub: null); + @override + _i2.CssStyleDeclaration get style => (super.noSuchMethod( + Invocation.getter(#style), + returnValue: _FakeCssStyleDeclaration_5()) as _i2.CssStyleDeclaration); + @override + set tabIndex(int? value) => + super.noSuchMethod(Invocation.setter(#tabIndex, value), + returnValueForMissingStub: null); + @override + set title(String? value) => + super.noSuchMethod(Invocation.setter(#title, value), + returnValueForMissingStub: null); + @override + set translate(bool? value) => + super.noSuchMethod(Invocation.setter(#translate, value), + returnValueForMissingStub: null); + @override + String get className => + (super.noSuchMethod(Invocation.getter(#className), returnValue: '') + as String); + @override + set className(String? value) => + super.noSuchMethod(Invocation.setter(#className, value), + returnValueForMissingStub: null); + @override + int get clientHeight => + (super.noSuchMethod(Invocation.getter(#clientHeight), returnValue: 0) + as int); + @override + int get clientWidth => + (super.noSuchMethod(Invocation.getter(#clientWidth), returnValue: 0) + as int); + @override + String get id => + (super.noSuchMethod(Invocation.getter(#id), returnValue: '') as String); + @override + set id(String? value) => super.noSuchMethod(Invocation.setter(#id, value), + returnValueForMissingStub: null); + @override + set slot(String? value) => super.noSuchMethod(Invocation.setter(#slot, value), + returnValueForMissingStub: null); + @override + String get tagName => + (super.noSuchMethod(Invocation.getter(#tagName), returnValue: '') + as String); + @override + _i2.ElementStream<_i2.Event> get onAbort => + (super.noSuchMethod(Invocation.getter(#onAbort), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforeCopy => + (super.noSuchMethod(Invocation.getter(#onBeforeCopy), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforeCut => + (super.noSuchMethod(Invocation.getter(#onBeforeCut), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforePaste => + (super.noSuchMethod(Invocation.getter(#onBeforePaste), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBlur => + (super.noSuchMethod(Invocation.getter(#onBlur), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onCanPlay => + (super.noSuchMethod(Invocation.getter(#onCanPlay), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onCanPlayThrough => + (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onChange => + (super.noSuchMethod(Invocation.getter(#onChange), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onClick => + (super.noSuchMethod(Invocation.getter(#onClick), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onContextMenu => + (super.noSuchMethod(Invocation.getter(#onContextMenu), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onCopy => + (super.noSuchMethod(Invocation.getter(#onCopy), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) + as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onCut => + (super.noSuchMethod(Invocation.getter(#onCut), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) + as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onDoubleClick => + (super.noSuchMethod(Invocation.getter(#onDoubleClick), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDrag => + (super.noSuchMethod(Invocation.getter(#onDrag), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragEnd => + (super.noSuchMethod(Invocation.getter(#onDragEnd), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragEnter => + (super.noSuchMethod(Invocation.getter(#onDragEnter), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragLeave => + (super.noSuchMethod(Invocation.getter(#onDragLeave), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragOver => + (super.noSuchMethod(Invocation.getter(#onDragOver), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragStart => + (super.noSuchMethod(Invocation.getter(#onDragStart), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDrop => + (super.noSuchMethod(Invocation.getter(#onDrop), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.Event> get onDurationChange => + (super.noSuchMethod(Invocation.getter(#onDurationChange), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onEmptied => + (super.noSuchMethod(Invocation.getter(#onEmptied), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onEnded => + (super.noSuchMethod(Invocation.getter(#onEnded), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onError => + (super.noSuchMethod(Invocation.getter(#onError), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFocus => + (super.noSuchMethod(Invocation.getter(#onFocus), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onInput => + (super.noSuchMethod(Invocation.getter(#onInput), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onInvalid => + (super.noSuchMethod(Invocation.getter(#onInvalid), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyDown => + (super.noSuchMethod(Invocation.getter(#onKeyDown), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) + as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyPress => + (super.noSuchMethod(Invocation.getter(#onKeyPress), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) + as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyUp => + (super.noSuchMethod(Invocation.getter(#onKeyUp), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) + as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onLoad => + (super.noSuchMethod(Invocation.getter(#onLoad), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onLoadedData => + (super.noSuchMethod(Invocation.getter(#onLoadedData), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onLoadedMetadata => + (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseDown => + (super.noSuchMethod(Invocation.getter(#onMouseDown), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseEnter => + (super.noSuchMethod(Invocation.getter(#onMouseEnter), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseLeave => + (super.noSuchMethod(Invocation.getter(#onMouseLeave), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseMove => + (super.noSuchMethod(Invocation.getter(#onMouseMove), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseOut => + (super.noSuchMethod(Invocation.getter(#onMouseOut), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseOver => + (super.noSuchMethod(Invocation.getter(#onMouseOver), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseUp => + (super.noSuchMethod(Invocation.getter(#onMouseUp), + returnValue: _FakeElementStream_6<_i2.MouseEvent>()) + as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.WheelEvent> get onMouseWheel => + (super.noSuchMethod(Invocation.getter(#onMouseWheel), + returnValue: _FakeElementStream_6<_i2.WheelEvent>()) + as _i2.ElementStream<_i2.WheelEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onPaste => + (super.noSuchMethod(Invocation.getter(#onPaste), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) + as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onPause => + (super.noSuchMethod(Invocation.getter(#onPause), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onPlay => + (super.noSuchMethod(Invocation.getter(#onPlay), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onPlaying => + (super.noSuchMethod(Invocation.getter(#onPlaying), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onRateChange => + (super.noSuchMethod(Invocation.getter(#onRateChange), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onReset => + (super.noSuchMethod(Invocation.getter(#onReset), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onResize => + (super.noSuchMethod(Invocation.getter(#onResize), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onScroll => + (super.noSuchMethod(Invocation.getter(#onScroll), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSearch => + (super.noSuchMethod(Invocation.getter(#onSearch), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSeeked => + (super.noSuchMethod(Invocation.getter(#onSeeked), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSeeking => + (super.noSuchMethod(Invocation.getter(#onSeeking), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSelect => + (super.noSuchMethod(Invocation.getter(#onSelect), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSelectStart => + (super.noSuchMethod(Invocation.getter(#onSelectStart), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onStalled => + (super.noSuchMethod(Invocation.getter(#onStalled), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSubmit => + (super.noSuchMethod(Invocation.getter(#onSubmit), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSuspend => + (super.noSuchMethod(Invocation.getter(#onSuspend), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onTimeUpdate => + (super.noSuchMethod(Invocation.getter(#onTimeUpdate), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchCancel => + (super.noSuchMethod(Invocation.getter(#onTouchCancel), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchEnd => + (super.noSuchMethod(Invocation.getter(#onTouchEnd), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchEnter => + (super.noSuchMethod(Invocation.getter(#onTouchEnter), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchLeave => + (super.noSuchMethod(Invocation.getter(#onTouchLeave), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchMove => + (super.noSuchMethod(Invocation.getter(#onTouchMove), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchStart => + (super.noSuchMethod(Invocation.getter(#onTouchStart), + returnValue: _FakeElementStream_6<_i2.TouchEvent>()) + as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TransitionEvent> get onTransitionEnd => + (super.noSuchMethod(Invocation.getter(#onTransitionEnd), + returnValue: _FakeElementStream_6<_i2.TransitionEvent>()) + as _i2.ElementStream<_i2.TransitionEvent>); + @override + _i2.ElementStream<_i2.Event> get onVolumeChange => + (super.noSuchMethod(Invocation.getter(#onVolumeChange), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onWaiting => + (super.noSuchMethod(Invocation.getter(#onWaiting), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFullscreenChange => + (super.noSuchMethod(Invocation.getter(#onFullscreenChange), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFullscreenError => + (super.noSuchMethod(Invocation.getter(#onFullscreenError), + returnValue: _FakeElementStream_6<_i2.Event>()) + as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.WheelEvent> get onWheel => + (super.noSuchMethod(Invocation.getter(#onWheel), + returnValue: _FakeElementStream_6<_i2.WheelEvent>()) + as _i2.ElementStream<_i2.WheelEvent>); + @override + List<_i2.Node> get nodes => + (super.noSuchMethod(Invocation.getter(#nodes), returnValue: <_i2.Node>[]) + as List<_i2.Node>); + @override + set nodes(Iterable<_i2.Node>? value) => + super.noSuchMethod(Invocation.setter(#nodes, value), + returnValueForMissingStub: null); + @override + List<_i2.Node> get childNodes => + (super.noSuchMethod(Invocation.getter(#childNodes), + returnValue: <_i2.Node>[]) as List<_i2.Node>); + @override + int get nodeType => + (super.noSuchMethod(Invocation.getter(#nodeType), returnValue: 0) as int); + @override + set text(String? value) => super.noSuchMethod(Invocation.setter(#text, value), + returnValueForMissingStub: null); + @override + String? getAttribute(String? name) => + (super.noSuchMethod(Invocation.method(#getAttribute, [name])) as String?); + @override + String? getAttributeNS(String? namespaceURI, String? name) => + (super.noSuchMethod( + Invocation.method(#getAttributeNS, [namespaceURI, name])) as String?); + @override + bool hasAttribute(String? name) => + (super.noSuchMethod(Invocation.method(#hasAttribute, [name]), + returnValue: false) as bool); + @override + bool hasAttributeNS(String? namespaceURI, String? name) => (super + .noSuchMethod(Invocation.method(#hasAttributeNS, [namespaceURI, name]), + returnValue: false) as bool); + @override + void removeAttribute(String? name) => + super.noSuchMethod(Invocation.method(#removeAttribute, [name]), + returnValueForMissingStub: null); + @override + void removeAttributeNS(String? namespaceURI, String? name) => super + .noSuchMethod(Invocation.method(#removeAttributeNS, [namespaceURI, name]), + returnValueForMissingStub: null); + @override + void setAttribute(String? name, Object? value) => + super.noSuchMethod(Invocation.method(#setAttribute, [name, value]), + returnValueForMissingStub: null); + @override + void setAttributeNS(String? namespaceURI, String? name, Object? value) => + super.noSuchMethod( + Invocation.method(#setAttributeNS, [namespaceURI, name, value]), + returnValueForMissingStub: null); + @override + _i2.ElementList querySelectorAll( + String? selectors) => + (super.noSuchMethod(Invocation.method(#querySelectorAll, [selectors]), + returnValue: _FakeElementList_7()) as _i2.ElementList); + @override + _i6.Future<_i2.ScrollState> setApplyScroll(String? nativeScrollBehavior) => + (super.noSuchMethod( + Invocation.method(#setApplyScroll, [nativeScrollBehavior]), + returnValue: Future<_i2.ScrollState>.value(_FakeScrollState_8())) + as _i6.Future<_i2.ScrollState>); + @override + _i6.Future<_i2.ScrollState> setDistributeScroll( + String? nativeScrollBehavior) => + (super.noSuchMethod( + Invocation.method(#setDistributeScroll, [nativeScrollBehavior]), + returnValue: Future<_i2.ScrollState>.value(_FakeScrollState_8())) + as _i6.Future<_i2.ScrollState>); + @override + Map getNamespacedAttributes(String? namespace) => (super + .noSuchMethod(Invocation.method(#getNamespacedAttributes, [namespace]), + returnValue: {}) as Map); + @override + _i2.CssStyleDeclaration getComputedStyle([String? pseudoElement]) => + (super.noSuchMethod(Invocation.method(#getComputedStyle, [pseudoElement]), + returnValue: _FakeCssStyleDeclaration_5()) + as _i2.CssStyleDeclaration); + @override + void appendText(String? text) => + super.noSuchMethod(Invocation.method(#appendText, [text]), + returnValueForMissingStub: null); + @override + void appendHtml(String? text, + {_i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer}) => + super.noSuchMethod( + Invocation.method(#appendHtml, [text], + {#validator: validator, #treeSanitizer: treeSanitizer}), + returnValueForMissingStub: null); + @override + void attached() => super.noSuchMethod(Invocation.method(#attached, []), + returnValueForMissingStub: null); + @override + void detached() => super.noSuchMethod(Invocation.method(#detached, []), + returnValueForMissingStub: null); + @override + void enteredView() => super.noSuchMethod(Invocation.method(#enteredView, []), + returnValueForMissingStub: null); + @override + List<_i3.Rectangle> getClientRects() => + (super.noSuchMethod(Invocation.method(#getClientRects, []), + returnValue: <_i3.Rectangle>[]) as List<_i3.Rectangle>); + @override + void leftView() => super.noSuchMethod(Invocation.method(#leftView, []), + returnValueForMissingStub: null); + @override + _i2.Animation animate(Iterable>? frames, + [dynamic timing]) => + (super.noSuchMethod(Invocation.method(#animate, [frames, timing]), + returnValue: _FakeAnimation_9()) as _i2.Animation); + @override + void attributeChanged(String? name, String? oldValue, String? newValue) => + super.noSuchMethod( + Invocation.method(#attributeChanged, [name, oldValue, newValue]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); + @override + void scrollIntoView([_i2.ScrollAlignment? alignment]) => + super.noSuchMethod(Invocation.method(#scrollIntoView, [alignment]), + returnValueForMissingStub: null); + @override + void insertAdjacentText(String? where, String? text) => + super.noSuchMethod(Invocation.method(#insertAdjacentText, [where, text]), + returnValueForMissingStub: null); + @override + void insertAdjacentHtml(String? where, String? html, + {_i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer}) => + super.noSuchMethod( + Invocation.method(#insertAdjacentHtml, [where, html], + {#validator: validator, #treeSanitizer: treeSanitizer}), + returnValueForMissingStub: null); + @override + _i2.Element insertAdjacentElement(String? where, _i2.Element? element) => + (super.noSuchMethod( + Invocation.method(#insertAdjacentElement, [where, element]), + returnValue: _FakeElement_10()) as _i2.Element); + @override + bool matches(String? selectors) => + (super.noSuchMethod(Invocation.method(#matches, [selectors]), + returnValue: false) as bool); + @override + bool matchesWithAncestors(String? selectors) => + (super.noSuchMethod(Invocation.method(#matchesWithAncestors, [selectors]), + returnValue: false) as bool); + @override + _i2.ShadowRoot createShadowRoot() => + (super.noSuchMethod(Invocation.method(#createShadowRoot, []), + returnValue: _FakeShadowRoot_11()) as _i2.ShadowRoot); + @override + _i3.Point offsetTo(_i2.Element? parent) => + (super.noSuchMethod(Invocation.method(#offsetTo, [parent]), + returnValue: _FakePoint_3()) as _i3.Point); + @override + _i2.DocumentFragment createFragment(String? html, + {_i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer}) => + (super.noSuchMethod( + Invocation.method(#createFragment, [html], + {#validator: validator, #treeSanitizer: treeSanitizer}), + returnValue: _FakeDocumentFragment_12()) as _i2.DocumentFragment); + @override + void setInnerHtml(String? html, + {_i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer}) => + super.noSuchMethod( + Invocation.method(#setInnerHtml, [html], + {#validator: validator, #treeSanitizer: treeSanitizer}), + returnValueForMissingStub: null); + @override + void blur() => super.noSuchMethod(Invocation.method(#blur, []), + returnValueForMissingStub: null); + @override + void click() => super.noSuchMethod(Invocation.method(#click, []), + returnValueForMissingStub: null); + @override + void focus() => super.noSuchMethod(Invocation.method(#focus, []), + returnValueForMissingStub: null); + @override + _i2.ShadowRoot attachShadow(Map? shadowRootInitDict) => + (super.noSuchMethod( + Invocation.method(#attachShadow, [shadowRootInitDict]), + returnValue: _FakeShadowRoot_11()) as _i2.ShadowRoot); + @override + _i2.Element? closest(String? selectors) => + (super.noSuchMethod(Invocation.method(#closest, [selectors])) + as _i2.Element?); + @override + List<_i2.Animation> getAnimations() => + (super.noSuchMethod(Invocation.method(#getAnimations, []), + returnValue: <_i2.Animation>[]) as List<_i2.Animation>); + @override + List getAttributeNames() => + (super.noSuchMethod(Invocation.method(#getAttributeNames, []), + returnValue: []) as List); + @override + _i3.Rectangle getBoundingClientRect() => + (super.noSuchMethod(Invocation.method(#getBoundingClientRect, []), + returnValue: _FakeRectangle_1()) as _i3.Rectangle); + @override + List<_i2.Node> getDestinationInsertionPoints() => + (super.noSuchMethod(Invocation.method(#getDestinationInsertionPoints, []), + returnValue: <_i2.Node>[]) as List<_i2.Node>); + @override + List<_i2.Node> getElementsByClassName(String? classNames) => (super + .noSuchMethod(Invocation.method(#getElementsByClassName, [classNames]), + returnValue: <_i2.Node>[]) as List<_i2.Node>); + @override + bool hasPointerCapture(int? pointerId) => + (super.noSuchMethod(Invocation.method(#hasPointerCapture, [pointerId]), + returnValue: false) as bool); + @override + void releasePointerCapture(int? pointerId) => + super.noSuchMethod(Invocation.method(#releasePointerCapture, [pointerId]), + returnValueForMissingStub: null); + @override + void requestPointerLock() => + super.noSuchMethod(Invocation.method(#requestPointerLock, []), + returnValueForMissingStub: null); + @override + void scroll([dynamic options_OR_x, num? y]) => + super.noSuchMethod(Invocation.method(#scroll, [options_OR_x, y]), + returnValueForMissingStub: null); + @override + void scrollBy([dynamic options_OR_x, num? y]) => + super.noSuchMethod(Invocation.method(#scrollBy, [options_OR_x, y]), + returnValueForMissingStub: null); + @override + void scrollTo([dynamic options_OR_x, num? y]) => + super.noSuchMethod(Invocation.method(#scrollTo, [options_OR_x, y]), + returnValueForMissingStub: null); + @override + void setPointerCapture(int? pointerId) => + super.noSuchMethod(Invocation.method(#setPointerCapture, [pointerId]), + returnValueForMissingStub: null); + @override + void requestFullscreen() => + super.noSuchMethod(Invocation.method(#requestFullscreen, []), + returnValueForMissingStub: null); + @override + void after(Object? nodes) => + super.noSuchMethod(Invocation.method(#after, [nodes]), + returnValueForMissingStub: null); + @override + void before(Object? nodes) => + super.noSuchMethod(Invocation.method(#before, [nodes]), + returnValueForMissingStub: null); + @override + _i2.Element? querySelector(String? selectors) => + (super.noSuchMethod(Invocation.method(#querySelector, [selectors])) + as _i2.Element?); + @override + void remove() => super.noSuchMethod(Invocation.method(#remove, []), + returnValueForMissingStub: null); + @override + _i2.Node replaceWith(_i2.Node? otherNode) => + (super.noSuchMethod(Invocation.method(#replaceWith, [otherNode]), + returnValue: _FakeNode_13()) as _i2.Node); + @override + void insertAllBefore(Iterable<_i2.Node>? newNodes, _i2.Node? refChild) => + super.noSuchMethod( + Invocation.method(#insertAllBefore, [newNodes, refChild]), + returnValueForMissingStub: null); + @override + _i2.Node append(_i2.Node? node) => + (super.noSuchMethod(Invocation.method(#append, [node]), + returnValue: _FakeNode_13()) as _i2.Node); + @override + _i2.Node clone(bool? deep) => + (super.noSuchMethod(Invocation.method(#clone, [deep]), + returnValue: _FakeNode_13()) as _i2.Node); + @override + bool contains(_i2.Node? other) => + (super.noSuchMethod(Invocation.method(#contains, [other]), + returnValue: false) as bool); + @override + _i2.Node getRootNode([Map? options]) => + (super.noSuchMethod(Invocation.method(#getRootNode, [options]), + returnValue: _FakeNode_13()) as _i2.Node); + @override + bool hasChildNodes() => + (super.noSuchMethod(Invocation.method(#hasChildNodes, []), + returnValue: false) as bool); + @override + _i2.Node insertBefore(_i2.Node? node, _i2.Node? child) => + (super.noSuchMethod(Invocation.method(#insertBefore, [node, child]), + returnValue: _FakeNode_13()) as _i2.Node); + @override + void addEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod( + Invocation.method(#addEventListener, [type, listener, useCapture]), + returnValueForMissingStub: null); + @override + void removeEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod( + Invocation.method(#removeEventListener, [type, listener, useCapture]), + returnValueForMissingStub: null); + @override + bool dispatchEvent(_i2.Event? event) => + (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), + returnValue: false) as bool); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Widget get widget => (super.noSuchMethod(Invocation.getter(#widget), + returnValue: _FakeWidget_14()) as _i4.Widget); + @override + bool get debugDoingBuild => (super + .noSuchMethod(Invocation.getter(#debugDoingBuild), returnValue: false) + as bool); + @override + _i4.InheritedWidget dependOnInheritedElement(_i4.InheritedElement? ancestor, + {Object? aspect}) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, [ancestor], {#aspect: aspect}), + returnValue: _FakeInheritedWidget_15()) as _i4.InheritedWidget); + @override + void visitAncestorElements(bool Function(_i4.Element)? visitor) => + super.noSuchMethod(Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null); + @override + void visitChildElements(_i4.ElementVisitor? visitor) => + super.noSuchMethod(Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null); + @override + _i5.DiagnosticsNode describeElement(String? name, + {_i5.DiagnosticsTreeStyle? style = + _i5.DiagnosticsTreeStyle.errorProperty}) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); + @override + _i5.DiagnosticsNode describeWidget(String? name, + {_i5.DiagnosticsTreeStyle? style = + _i5.DiagnosticsTreeStyle.errorProperty}) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); + @override + List<_i5.DiagnosticsNode> describeMissingAncestor( + {Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], + {#expectedAncestorType: expectedAncestorType}), + returnValue: <_i5.DiagnosticsNode>[]) as List<_i5.DiagnosticsNode>); + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod(Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); + @override + String toString() => super.toString(); +} + +/// A class which mocks [CreationParams]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCreationParams extends _i1.Mock implements _i7.CreationParams { + MockCreationParams() { + _i1.throwOnMissingStub(this); + } + + @override + Set get javascriptChannelNames => + (super.noSuchMethod(Invocation.getter(#javascriptChannelNames), + returnValue: {}) as Set); + @override + _i8.AutoMediaPlaybackPolicy get autoMediaPlaybackPolicy => + (super.noSuchMethod(Invocation.getter(#autoMediaPlaybackPolicy), + returnValue: _i8.AutoMediaPlaybackPolicy + .require_user_action_for_all_media_types) + as _i8.AutoMediaPlaybackPolicy); + @override + List<_i7.WebViewCookie> get cookies => + (super.noSuchMethod(Invocation.getter(#cookies), + returnValue: <_i7.WebViewCookie>[]) as List<_i7.WebViewCookie>); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewPlatformCallbacksHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformCallbacksHandler extends _i1.Mock + implements _i9.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + (super.noSuchMethod( + Invocation.method(#onNavigationRequest, [], + {#url: url, #isForMainFrame: isForMainFrame}), + returnValue: Future.value(false)) as _i6.FutureOr); + @override + void onPageStarted(String? url) => + super.noSuchMethod(Invocation.method(#onPageStarted, [url]), + returnValueForMissingStub: null); + @override + void onPageFinished(String? url) => + super.noSuchMethod(Invocation.method(#onPageFinished, [url]), + returnValueForMissingStub: null); + @override + void onProgress(int? progress) => + super.noSuchMethod(Invocation.method(#onProgress, [progress]), + returnValueForMissingStub: null); + @override + void onWebResourceError(_i7.WebResourceError? error) => + super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [HttpRequestFactory]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequestFactory extends _i1.Mock + implements _i10.HttpRequestFactory { + MockHttpRequestFactory() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future<_i2.HttpRequest> request(String? url, + {String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void Function(_i2.ProgressEvent)? onProgress}) => + (super.noSuchMethod( + Invocation.method(#request, [ + url + ], { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress + }), + returnValue: Future<_i2.HttpRequest>.value(_FakeHttpRequest_17())) + as _i6.Future<_i2.HttpRequest>); + @override + String toString() => super.toString(); +} + +/// A class which mocks [HttpRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { + MockHttpRequest() { + _i1.throwOnMissingStub(this); + } + + @override + Map get responseHeaders => + (super.noSuchMethod(Invocation.getter(#responseHeaders), + returnValue: {}) as Map); + @override + int get readyState => + (super.noSuchMethod(Invocation.getter(#readyState), returnValue: 0) + as int); + @override + String get responseType => + (super.noSuchMethod(Invocation.getter(#responseType), returnValue: '') + as String); + @override + set responseType(String? value) => + super.noSuchMethod(Invocation.setter(#responseType, value), + returnValueForMissingStub: null); + @override + set timeout(int? value) => + super.noSuchMethod(Invocation.setter(#timeout, value), + returnValueForMissingStub: null); + @override + _i2.HttpRequestUpload get upload => + (super.noSuchMethod(Invocation.getter(#upload), + returnValue: _FakeHttpRequestUpload_18()) as _i2.HttpRequestUpload); + @override + set withCredentials(bool? value) => + super.noSuchMethod(Invocation.setter(#withCredentials, value), + returnValueForMissingStub: null); + @override + _i6.Stream<_i2.Event> get onReadyStateChange => + (super.noSuchMethod(Invocation.getter(#onReadyStateChange), + returnValue: Stream<_i2.Event>.empty()) as _i6.Stream<_i2.Event>); + @override + _i6.Stream<_i2.ProgressEvent> get onAbort => + (super.noSuchMethod(Invocation.getter(#onAbort), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onError => + (super.noSuchMethod(Invocation.getter(#onError), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoad => + (super.noSuchMethod(Invocation.getter(#onLoad), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoadEnd => + (super.noSuchMethod(Invocation.getter(#onLoadEnd), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoadStart => + (super.noSuchMethod(Invocation.getter(#onLoadStart), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onProgress => + (super.noSuchMethod(Invocation.getter(#onProgress), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onTimeout => + (super.noSuchMethod(Invocation.getter(#onTimeout), + returnValue: Stream<_i2.ProgressEvent>.empty()) + as _i6.Stream<_i2.ProgressEvent>); + @override + _i2.Events get on => + (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents_19()) + as _i2.Events); + @override + void open(String? method, String? url, + {bool? async, String? user, String? password}) => + super.noSuchMethod( + Invocation.method(#open, [method, url], + {#async: async, #user: user, #password: password}), + returnValueForMissingStub: null); + @override + void abort() => super.noSuchMethod(Invocation.method(#abort, []), + returnValueForMissingStub: null); + @override + String getAllResponseHeaders() => + (super.noSuchMethod(Invocation.method(#getAllResponseHeaders, []), + returnValue: '') as String); + @override + String? getResponseHeader(String? name) => + (super.noSuchMethod(Invocation.method(#getResponseHeader, [name])) + as String?); + @override + void overrideMimeType(String? mime) => + super.noSuchMethod(Invocation.method(#overrideMimeType, [mime]), + returnValueForMissingStub: null); + @override + void send([dynamic body_OR_data]) => + super.noSuchMethod(Invocation.method(#send, [body_OR_data]), + returnValueForMissingStub: null); + @override + void setRequestHeader(String? name, String? value) => + super.noSuchMethod(Invocation.method(#setRequestHeader, [name, value]), + returnValueForMissingStub: null); + @override + void addEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod( + Invocation.method(#addEventListener, [type, listener, useCapture]), + returnValueForMissingStub: null); + @override + void removeEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod( + Invocation.method(#removeEventListener, [type, listener, useCapture]), + returnValueForMissingStub: null); + @override + bool dispatchEvent(_i2.Event? event) => + (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), + returnValue: false) as bool); + @override + String toString() => super.toString(); +} From d7f7b89adf34f92eee8f72de263d5ce8ce752068 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Dec 2021 19:48:51 -0500 Subject: [PATCH 233/259] [webview_flutter] Default Android to hybrid composition (#4576) Switches the default implemention on Android from virtual display to hybrid composition. This is a breaking change. Part of https://github.com/flutter/flutter/issues/93335 --- .../webview_flutter/CHANGELOG.md | 6 + .../webview_flutter/webview_flutter/README.md | 45 ++--- .../webview_flutter_test.dart | 177 ++---------------- .../webview_flutter/lib/src/webview.dart | 6 +- .../webview_flutter/pubspec.yaml | 2 +- 5 files changed, 43 insertions(+), 193 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 4906003a2e02..aa117c9d72a8 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.0 + +* **BREAKING CHANGE**: On Android, hybrid composition (SurfaceAndroidWebView) + is now the default. The previous default, virtual display, can be specified + with `WebView.platform = AndroidWebView()` + ## 2.8.0 * Adds support for the `loadFlutterAsset` method. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index c3982eab2bad..8387eb685b7d 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -15,68 +15,63 @@ You can now include a WebView widget in your widget tree. See the widget's Dartdoc for more details on how to use the widget. ## Android Platform Views -The WebView is relying on +This plugin uses [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed -the Android’s webview within the Flutter app. It supports two modes: *Virtual displays* (the current default) and *Hybrid composition*. +the Android’s webview within the Flutter app. It supports two modes: +*hybrid composition* (the current default) and *virtual display*. Here are some points to consider when choosing between the two: -* *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple -[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) -* *Hybrid composition* mode requires Android SDK 19+ while *Virtual displays* mode requires Android SDK 20+ -* *Hybrid composition* mode has [performance limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions +* *Hybrid composition* has built-in keyboard support while *virtual display* has multiple +[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). +* *Hybrid composition* requires Android SDK 19+ while *virtual display* requires Android SDK 20+. +* *Hybrid composition* and *virtual display* have different + [performance tradeoffs](https://flutter.dev/docs/development/platform-integration/platform-views#performance). -| | Hybrid composition | Virtual displays | -| --------------------------- | ------------------- | ---------------- | -| **Full keyboard supoport** | yes | no | -| **Android SDK support** | 19+ | 20+ | -| **Full performance** | Android 10+ | always | -| **The default** | no | yes | -### Using Virtual displays +### Using Hybrid Composition -The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): +The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19: ```groovy android { defaultConfig { - minSdkVersion 20 + minSdkVersion 19 } } ``` +### Using Virtual displays -### Using Hybrid Composition - -1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 19): +1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): ```groovy android { defaultConfig { - minSdkVersion 19 + minSdkVersion 20 } } ``` -2. Set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. +2. Set `WebView.platform = AndroidWebView();` in `initState()`. For example: - + ```dart import 'dart:io'; - + import 'package:webview_flutter/webview_flutter.dart'; class WebViewExample extends StatefulWidget { @override WebViewExampleState createState() => WebViewExampleState(); } - + class WebViewExampleState extends State { @override void initState() { super.initState(); - // Enable hybrid composition. - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + // Enable virtual display. + if (Platform.isAndroid) WebView.platform = AndroidWebView(); } @override 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 cc74afd8a745..4c2d1fb0f4ff 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 @@ -860,192 +860,41 @@ void main() { }, skip: Platform.isAndroid && _skipDueToIssue86757); }); - group('SurfaceAndroidWebView', () { + // Minimial end-to-end testing of the legacy Android implementation. + group('AndroidWebView (virtual display)', () { setUpAll(() { - WebView.platform = SurfaceAndroidWebView(); + WebView.platform = AndroidWebView(); }); tearDownAll(() { WebView.platform = null; }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - const String scrollTestPage = ''' - - - - - - -
- - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); + testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - + final Completer loadCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + key: GlobalKey(), + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, onPageFinished: (String url) { - pageLoaded.complete(null); + loadCompleter.complete(); }, ), ), ); - - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - await tester.pumpAndSettle(const Duration(seconds: 3)); - - // Check scrollTo() - const int X_SCROLL = 123; - const int Y_SCROLL = 321; - - await controller.scrollTo(X_SCROLL, Y_SCROLL); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); - expect(X_SCROLL, scrollPosX); - expect(Y_SCROLL, scrollPosY); - - // Check scrollBy() (on top of scrollTo()) - await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(X_SCROLL * 2, scrollPosX); - expect(Y_SCROLL * 2, scrollPosY); - }, skip: !Platform.isAndroid || _skipDueToIssue86757); - - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('inputs are scrolled into view when focused', - (WidgetTester tester) async { - const String scrollTestPage = ''' - - - - - - -
- - - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.runAsync(() async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 200, - height: 200, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ), - ); - await Future.delayed(const Duration(milliseconds: 20)); - await tester.pump(); - }); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - final String viewportRectJSON = await _runJavascriptReturningResult( - controller, 'JSON.stringify(viewport.getBoundingClientRect())'); - final Map viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON) as Map; - - // Check that the input is originally outside of the viewport. - - final String initialInputClientRectJSON = - await _runJavascriptReturningResult( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON) as Map; - - expect( - initialInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isFalse); - - await controller.runJavascript('inputEl.focus()'); - - // Check that focusing the input brought it into view. - - final String lastInputClientRectJSON = - await _runJavascriptReturningResult( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON) as Map; - - expect( - lastInputClientRectRelativeToViewport['top'] >= - viewportRectRelativeToViewport['top'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isTrue); - - expect( - lastInputClientRectRelativeToViewport['left'] >= - viewportRectRelativeToViewport['left'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['right'] <= - viewportRectRelativeToViewport['right'], - isTrue); - }, skip: !Platform.isAndroid || _skipDueToIssue86757); - }); + await loadCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); group('NavigationDelegate', () { const String blankPage = ''; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 7b907a5f8bb2..a17702750d62 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -9,8 +9,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_android/webview_android.dart'; import 'package:webview_flutter_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'; @@ -119,12 +119,12 @@ class WebView extends StatefulWidget { /// The WebView platform that's used by this WebView. /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + /// The default value is [SurfaceAndroidWebView] on Android and [CupertinoWebView] on iOS. static WebViewPlatform get platform { if (_platform == null) { switch (defaultTargetPlatform) { case TargetPlatform.android: - _platform = AndroidWebView(); + _platform = SurfaceAndroidWebView(); break; case TargetPlatform.iOS: _platform = CupertinoWebView(); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 4ac5e1d8add3..4af446248307 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/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.8.0 +version: 3.0.0 environment: sdk: ">=2.14.0 <3.0.0" From 3e29e9173980253ab31c210487dc6221fd6b03e2 Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:50:23 -0800 Subject: [PATCH 234/259] renew cirrus key (#4592) --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 64bbfdf1b9cc..95bd6092674b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,4 +1,4 @@ -gcp_credentials: ENCRYPTED[!48cff44dd32e9cc412d4d381c7fe68d373ca04cf2639f8192d21cb1a9ab5e21129651423a1cf88f3fd7fe2125c1cabd9!] +gcp_credentials: ENCRYPTED[!cc769765170bebc37e0556e2da5915ca64ee37f4ec8c966ec147e2f59578b476c99e457eafce4e2f8b1a4e305f7096b8!] # Don't run on release tags since it creates O(n^2) tasks where n is the # number of plugins From 1f0d2786c2b8980756c436500bf723ee4d5d08ef Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 8 Dec 2021 15:16:00 -0500 Subject: [PATCH 235/259] [path_provider] Remove platform registration (#4540) The plugin requires Flutter 2.5, which supports auto-registration of Dart implementation code in platform packages, and the packages already do auto-registration. This registration is unused, so can be removed. --- .../path_provider/path_provider/CHANGELOG.md | 3 +- .../path_provider/lib/path_provider.dart | 29 ++----------------- .../path_provider/path_provider/pubspec.yaml | 6 ++-- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 237815de6ce4..bc349f7b86fa 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.0.8 * Updates example app Android compileSdkVersion to 31. +* Removes obsolete manual registration of Windows and Linux implementations. ## 2.0.7 diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index e690b7f92960..e89d29dc0036 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -2,14 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io' show Directory, Platform; +import 'dart:io' show Directory; -import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; -import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -// ignore: implementation_imports -import 'package:path_provider_platform_interface/src/method_channel_path_provider.dart'; -import 'package:path_provider_windows/path_provider_windows.dart'; export 'package:path_provider_platform_interface/path_provider_platform_interface.dart' show StorageDirectory; @@ -18,8 +14,6 @@ export 'package:path_provider_platform_interface/path_provider_platform_interfac @Deprecated('This is no longer necessary, and is now a no-op') set disablePathProviderPlatformOverride(bool override) {} -bool _manualDartRegistrationNeeded = true; - /// An exception thrown when a directory that should always be available on /// the current platform cannot be obtained. class MissingPlatformDirectoryException implements Exception { @@ -41,24 +35,7 @@ class MissingPlatformDirectoryException implements Exception { } } -PathProviderPlatform get _platform { - // TODO(egarciad): Remove once auto registration lands on Flutter stable. - // https://github.com/flutter/flutter/issues/81421. - if (_manualDartRegistrationNeeded) { - // Only do the initial registration if it hasn't already been overridden - // with a non-default instance. - if (!kIsWeb && PathProviderPlatform.instance is MethodChannelPathProvider) { - if (Platform.isLinux) { - PathProviderPlatform.instance = PathProviderLinux(); - } else if (Platform.isWindows) { - PathProviderPlatform.instance = PathProviderWindows(); - } - } - _manualDartRegistrationNeeded = false; - } - - return PathProviderPlatform.instance; -} +PathProviderPlatform get _platform => PathProviderPlatform.instance; /// Path to the temporary directory on the device that is not backed up and is /// suitable for storing caches of downloaded files. diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index a31e804fa357..d8bca0853cc7 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.7 +version: 2.0.8 environment: sdk: ">=2.14.0 <3.0.0" @@ -27,10 +27,10 @@ dependencies: sdk: flutter path_provider_android: ^2.0.6 path_provider_ios: ^2.0.6 - path_provider_linux: ^2.0.0 + path_provider_linux: ^2.0.1 path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 - path_provider_windows: ^2.0.0 + path_provider_windows: ^2.0.2 dev_dependencies: flutter_driver: From d5e200badd422a70a074f9d8e2a03490729f1fbe Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 9 Dec 2021 08:23:03 -0500 Subject: [PATCH 236/259] [shared_preferences] Remove platform registration (#4596) The plugin requires Flutter 2.5, which supports auto-registration of Dart implementation code in platform packages, and the packages already do auto-registration. This registration is unused, so can be removed. Fixes https://github.com/flutter/flutter/issues/81421 --- .../shared_preferences/CHANGELOG.md | 5 +++- .../lib/shared_preferences.dart | 28 ++----------------- .../shared_preferences/pubspec.yaml | 6 ++-- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 818266f92597..cc29dd98fc89 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,8 +1,11 @@ +## 2.0.10 + +* Removes obsolete manual registration of Windows and Linux implementations. + ## 2.0.9 * Fixes newly enabled analyzer options. * Updates example app Android compileSdkVersion to 31. - * Moved Android and iOS implementations to federated packages. ## 2.0.8 diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index b0a11fc8b13a..8e9f08729355 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -3,14 +3,9 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io' show Platform; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:meta/meta.dart'; -import 'package:shared_preferences_linux/shared_preferences_linux.dart'; -import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; -import 'package:shared_preferences_windows/shared_preferences_windows.dart'; /// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing /// a persistent store for simple data. @@ -21,28 +16,9 @@ class SharedPreferences { static const String _prefix = 'flutter.'; static Completer? _completer; - static bool _manualDartRegistrationNeeded = true; - - static SharedPreferencesStorePlatform get _store { - // TODO(egarciad): Remove once auto registration lands on Flutter stable. - // https://github.com/flutter/flutter/issues/81421. - if (_manualDartRegistrationNeeded) { - // Only do the initial registration if it hasn't already been overridden - // with a non-default instance. - if (!kIsWeb && - SharedPreferencesStorePlatform.instance - is MethodChannelSharedPreferencesStore) { - if (Platform.isLinux) { - SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); - } else if (Platform.isWindows) { - SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); - } - } - _manualDartRegistrationNeeded = false; - } - return SharedPreferencesStorePlatform.instance; - } + static SharedPreferencesStorePlatform get _store => + SharedPreferencesStorePlatform.instance; /// Loads and parses the [SharedPreferences] for this app from disk. /// diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 732a62adc5dc..156e2e230565 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.9 +version: 2.0.10 environment: sdk: ">=2.14.0 <3.0.0" @@ -31,11 +31,11 @@ dependencies: meta: ^1.3.0 shared_preferences_android: ^2.0.8 shared_preferences_ios: ^2.0.8 - shared_preferences_linux: ^2.0.0 + shared_preferences_linux: ^2.0.1 shared_preferences_macos: ^2.0.0 shared_preferences_platform_interface: ^2.0.0 shared_preferences_web: ^2.0.0 - shared_preferences_windows: ^2.0.0 + shared_preferences_windows: ^2.0.1 dev_dependencies: flutter_driver: From 2f72b8fba3814595a84bb4d01d538e9d75ef8cd2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 9 Dec 2021 11:00:02 -0500 Subject: [PATCH 237/259] Match repo state to ignored files list (#4602) There are a number of file types that were added to the root `.gitignore` list at some point without all of the existing checked-in copies of those files being removed, which is now a `pub publish` error. This - removes all of the files that are on the .gitignore list but are checked in, and - adds generated_plugin_registrant.h to the ignore list, since all of the other generated registrant files are there; it doesn't make sense to remove the .cc files but not the .h files. --- .gitignore | 5 ++-- packages/camera/camera/camera_android.iml | 12 -------- packages/camera/camera/example/android.iml | 12 -------- .../camera/camera/example/camera_example.iml | 15 ---------- .../camera/example/camera_example_android.iml | 12 -------- .../google_maps_flutter/example/android.iml | 12 -------- .../example/google_maps_flutter_example.iml | 16 ---------- .../google_maps_flutter_example_android.iml | 26 ----------------- .../google_mobile_maps.iml | 19 ------------ .../google_mobile_maps_android.iml | 29 ------------------- .../google_sign_in/example/android.iml | 12 -------- .../google_sign_in/example/example.iml | 15 ---------- .../example/google_sign_in_example.iml | 15 ---------- .../image_picker/example/android.iml | 12 -------- .../example/image_picker_example.iml | 16 ---------- packages/local_auth/example/android.iml | 12 -------- .../local_auth/example/local_auth_example.iml | 15 ---------- .../example/local_auth_example_android.iml | 12 -------- packages/local_auth/local_auth_android.iml | 12 -------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../Flutter/GeneratedPluginRegistrant.swift | 12 -------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../Flutter/GeneratedPluginRegistrant.swift | 12 -------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../Flutter/GeneratedPluginRegistrant.swift | 12 -------- .../flutter/generated_plugin_registrant.cc | 11 ------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 15 ---------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../Flutter/GeneratedPluginRegistrant.swift | 12 -------- .../example/url_launcher_example.iml | 16 ---------- .../flutter/generated_plugin_registrant.cc | 14 --------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../flutter/generated_plugin_registrant.cc | 15 ---------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../Flutter/GeneratedPluginRegistrant.swift | 12 -------- .../flutter/generated_plugin_registrant.cc | 14 --------- .../flutter/generated_plugin_registrant.h | 15 ---------- .../video_player/example/android.iml | 12 -------- .../example/video_player_example.iml | 15 ---------- .../example/video_player_example_android.iml | 12 -------- .../video_player/video_player_android.iml | 12 -------- 53 files changed, 3 insertions(+), 729 deletions(-) delete mode 100644 packages/camera/camera/camera_android.iml delete mode 100644 packages/camera/camera/example/android.iml delete mode 100644 packages/camera/camera/example/camera_example.iml delete mode 100644 packages/camera/camera/example/camera_example_android.iml delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android.iml delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml delete mode 100644 packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml delete mode 100644 packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml delete mode 100644 packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml delete mode 100644 packages/google_sign_in/google_sign_in/example/android.iml delete mode 100644 packages/google_sign_in/google_sign_in/example/example.iml delete mode 100644 packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml delete mode 100755 packages/image_picker/image_picker/example/android.iml delete mode 100755 packages/image_picker/image_picker/example/image_picker_example.iml delete mode 100644 packages/local_auth/example/android.iml delete mode 100644 packages/local_auth/example/local_auth_example.iml delete mode 100644 packages/local_auth/example/local_auth_example_android.iml delete mode 100644 packages/local_auth/local_auth_android.iml delete mode 100644 packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 packages/url_launcher/url_launcher/example/url_launcher_example.iml delete mode 100644 packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h delete mode 100644 packages/video_player/video_player/example/android.iml delete mode 100644 packages/video_player/video_player/example/video_player_example.iml delete mode 100644 packages/video_player/video_player/example/video_player_example_android.iml delete mode 100644 packages/video_player/video_player/video_player_android.iml diff --git a/.gitignore b/.gitignore index f4fa0b9b7795..8eaeaff8de55 100644 --- a/.gitignore +++ b/.gitignore @@ -34,11 +34,12 @@ gradle-wrapper.jar .flutter-plugins-dependencies *.iml +generated_plugin_registrant.cc +generated_plugin_registrant.h generated_plugin_registrant.dart +GeneratedPluginRegistrant.java GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m -generated_plugin_registrant.cc -GeneratedPluginRegistrant.java GeneratedPluginRegistrant.swift build/ .flutter-plugins diff --git a/packages/camera/camera/camera_android.iml b/packages/camera/camera/camera_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/camera_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera/example/android.iml b/packages/camera/camera/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera/example/camera_example.iml b/packages/camera/camera/example/camera_example.iml deleted file mode 100644 index dafb001137cd..000000000000 --- a/packages/camera/camera/example/camera_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/camera/camera/example/camera_example_android.iml b/packages/camera/camera/example/camera_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/example/camera_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android.iml b/packages/google_maps_flutter/google_maps_flutter/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml b/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml deleted file mode 100644 index 8070e6469054..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml b/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml deleted file mode 100644 index 0ca70ed93eaf..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml b/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml deleted file mode 100644 index 0fbaf2c3a822..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml b/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml deleted file mode 100644 index 0ebb6c9fe763..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/google_sign_in/example/android.iml b/packages/google_sign_in/google_sign_in/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/google_sign_in/google_sign_in/example/example.iml b/packages/google_sign_in/google_sign_in/example/example.iml deleted file mode 100644 index c4447024fe3c..000000000000 --- a/packages/google_sign_in/google_sign_in/example/example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml b/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/image_picker/image_picker/example/android.iml b/packages/image_picker/image_picker/example/android.iml deleted file mode 100755 index 462b903e05b6..000000000000 --- a/packages/image_picker/image_picker/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/image_picker/image_picker/example/image_picker_example.iml b/packages/image_picker/image_picker/example/image_picker_example.iml deleted file mode 100755 index 1ae40a0f7f54..000000000000 --- a/packages/image_picker/image_picker/example/image_picker_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/local_auth/example/android.iml b/packages/local_auth/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/local_auth/example/local_auth_example.iml b/packages/local_auth/example/local_auth_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/local_auth/example/local_auth_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/local_auth/example/local_auth_example_android.iml b/packages/local_auth/example/local_auth_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/example/local_auth_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/local_auth/local_auth_android.iml b/packages/local_auth/local_auth_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/local_auth_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 0d56f519c923..000000000000 --- a/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import path_provider_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) -} diff --git a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 287b6a9d2769..000000000000 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import shared_preferences_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -} diff --git a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 287b6a9d2769..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import shared_preferences_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe970f..000000000000 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 8236f5728c63..000000000000 --- a/packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/packages/url_launcher/url_launcher/example/url_launcher_example.iml b/packages/url_launcher/url_launcher/example/url_launcher_example.iml deleted file mode 100644 index f2b096d12510..000000000000 --- a/packages/url_launcher/url_launcher/example/url_launcher_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4f7884874da7..000000000000 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe970f..000000000000 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 8236f5728c63..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4f7884874da7..000000000000 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/video_player/video_player/example/android.iml b/packages/video_player/video_player/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/video_player/video_player/example/video_player_example.iml b/packages/video_player/video_player/example/video_player_example.iml deleted file mode 100644 index dafb001137cd..000000000000 --- a/packages/video_player/video_player/example/video_player_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/video_player/video_player/example/video_player_example_android.iml b/packages/video_player/video_player/example/video_player_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/example/video_player_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/video_player/video_player/video_player_android.iml b/packages/video_player/video_player/video_player_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/video_player_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - From 33968c0e9e223ca64dbd6e07dc4addf81cb7269f Mon Sep 17 00:00:00 2001 From: BeMacized Date: Thu, 9 Dec 2021 20:44:22 +0100 Subject: [PATCH 238/259] [webview_flutter] Change import of FLTCookieManager.h to relative import (#4600) --- .../webview_flutter/webview_flutter_wkwebview/CHANGELOG.md | 4 ++++ .../webview_flutter_wkwebview/ios/Classes/FlutterWebView.h | 3 ++- .../webview_flutter/webview_flutter_wkwebview/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 0aaf4bcf57aa..db8338df5565 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.1 + +* Fixes header import for cookie manager to be relative only. + ## 2.7.0 * Adds implementation of the `loadFlutterAsset` method from the platform interface. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 145a772c5015..0dae94290a0d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -2,10 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import #import #import +#import "FLTCookieManager.h" + NS_ASSUME_NONNULL_BEGIN /** diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 08e98b11d4bf..d39f42b0ecdf 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/master/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.7.0 +version: 2.7.1 environment: sdk: ">=2.14.0 <3.0.0" From 64c222d368bab925fe855c201a6b5f49f82bb7eb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 9 Dec 2021 17:02:24 -0500 Subject: [PATCH 239/259] Move custom analysis options into sub-packages (#4603) When the legacy analysis options were bulk-added to the repository to allow transitioning incrementally to the Flutter analysis options, they were added to top-level plugin groups. This makes it harder to migrate incrementally, and also means that when we add a new sub-package (e.g, a web or desktop implementation) it's automatically using the legacy options. This moves all the legacy option files into each individual package, and updates the allow list for the tool accordingly, so that migration can be done package by package. --- packages/camera/analysis_options.yaml | 1 - packages/camera/camera/analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../camera/camera_web/analysis_options.yaml | 1 + packages/camera/camera_web/pubspec.yaml | 2 +- .../google_maps_flutter/analysis_options.yaml | 1 - .../google_maps_flutter/analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../google_maps_flutter_web/pubspec.yaml | 2 +- packages/google_sign_in/analysis_options.yaml | 1 - .../google_sign_in/analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../google_sign_in_web/analysis_options.yaml | 1 + .../google_sign_in_web/pubspec.yaml | 2 +- packages/image_picker/analysis_options.yaml | 1 - .../image_picker/analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../image_picker_for_web/pubspec.yaml | 2 +- .../analysis_options.yaml | 1 + packages/video_player/analysis_options.yaml | 1 - .../video_player/analysis_options.yaml | 1 + .../analysis_options.yaml | 1 + .../video_player_web/analysis_options.yaml | 1 + .../video_player_web/pubspec.yaml | 2 +- script/configs/custom_analysis.yaml | 28 ++++++++++++------- 26 files changed, 38 insertions(+), 20 deletions(-) delete mode 100644 packages/camera/analysis_options.yaml create mode 100644 packages/camera/camera/analysis_options.yaml create mode 100644 packages/camera/camera_platform_interface/analysis_options.yaml create mode 100644 packages/camera/camera_web/analysis_options.yaml delete mode 100644 packages/google_maps_flutter/analysis_options.yaml create mode 100644 packages/google_maps_flutter/google_maps_flutter/analysis_options.yaml create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml delete mode 100644 packages/google_sign_in/analysis_options.yaml create mode 100644 packages/google_sign_in/google_sign_in/analysis_options.yaml create mode 100644 packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml create mode 100644 packages/google_sign_in/google_sign_in_web/analysis_options.yaml delete mode 100644 packages/image_picker/analysis_options.yaml create mode 100644 packages/image_picker/image_picker/analysis_options.yaml create mode 100644 packages/image_picker/image_picker_for_web/analysis_options.yaml create mode 100644 packages/image_picker/image_picker_platform_interface/analysis_options.yaml delete mode 100644 packages/video_player/analysis_options.yaml create mode 100644 packages/video_player/video_player/analysis_options.yaml create mode 100644 packages/video_player/video_player_platform_interface/analysis_options.yaml create mode 100644 packages/video_player/video_player_web/analysis_options.yaml diff --git a/packages/camera/analysis_options.yaml b/packages/camera/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/camera/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/camera/camera/analysis_options.yaml b/packages/camera/camera/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/camera/camera/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/camera/camera_platform_interface/analysis_options.yaml b/packages/camera/camera_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/camera/camera_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/camera/camera_web/analysis_options.yaml b/packages/camera/camera_web/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/camera/camera_web/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index f37500ad6e22..ce09f7ed9d64 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -22,9 +22,9 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + pedantic: ^1.11.1 stream_transform: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 diff --git a/packages/google_maps_flutter/analysis_options.yaml b/packages/google_maps_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/google_maps_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 1f5fe4d96ccc..080726ee6d85 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -24,13 +24,13 @@ dependencies: google_maps_flutter_platform_interface: ^2.1.2 google_maps: ^5.2.0 meta: ^1.3.0 + pedantic: ^1.10.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 # The example deliberately includes limited-use secrets. false_secrets: diff --git a/packages/google_sign_in/analysis_options.yaml b/packages/google_sign_in/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/google_sign_in/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/google_sign_in/analysis_options.yaml b/packages/google_sign_in/google_sign_in/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml b/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/google_sign_in_web/analysis_options.yaml b/packages/google_sign_in/google_sign_in_web/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml 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 723dbe9ce56f..7388ea138883 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -25,8 +25,8 @@ dependencies: google_sign_in_platform_interface: ^2.0.0 js: ^0.6.3 meta: ^1.3.0 + pedantic: ^1.10.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/image_picker/analysis_options.yaml b/packages/image_picker/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/image_picker/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker/analysis_options.yaml b/packages/image_picker/image_picker/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/image_picker/image_picker/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker_for_web/analysis_options.yaml b/packages/image_picker/image_picker_for_web/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index d0d97011c9ec..a104dce66fe9 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: sdk: flutter image_picker_platform_interface: ^2.2.0 meta: ^1.3.0 + pedantic: ^1.10.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/image_picker/image_picker_platform_interface/analysis_options.yaml b/packages/image_picker/image_picker_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/video_player/analysis_options.yaml b/packages/video_player/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/video_player/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player/analysis_options.yaml b/packages/video_player/video_player/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/video_player/video_player/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player_platform_interface/analysis_options.yaml b/packages/video_player/video_player_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player_web/analysis_options.yaml b/packages/video_player/video_player_web/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/video_player/video_player_web/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index b401673c628d..ac0754b1a5d0 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -22,9 +22,9 @@ dependencies: flutter_web_plugins: sdk: flutter meta: ^1.3.0 + pedantic: ^1.10.0 video_player_platform_interface: ^4.2.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 81bb0f5db59c..e991e5e1446c 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -4,10 +4,6 @@ # analysis_options.yaml based on flutter/flutter, rather than the original # rules based on pedantic (now at analysis_options_legacy.yaml). # -# DO NOT add new entries to the list, unless it is to push the legacy rules -# from a top-level package into more specific packages in order to incrementally -# migrate a federated plugin. -# # DO NOT move or delete this file without updating # https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh # which references this file from source, but out-of-repo. @@ -15,11 +11,23 @@ # TODO(ecosystem): Remove everything from this list. See: # https://github.com/flutter/flutter/issues/76229 -- camera -- google_maps_flutter -- google_sign_in -- image_picker -- in_app_purchase +- camera/camera +- camera/camera_platform_interface +- camera/camera_web +- google_maps_flutter/google_maps_flutter +- google_maps_flutter/google_maps_flutter_platform_interface +- google_maps_flutter/google_maps_flutter_web +- google_sign_in/google_sign_in +- google_sign_in/google_sign_in_platform_interface +- google_sign_in/google_sign_in_web +- image_picker/image_picker +- image_picker/image_picker_for_web +- image_picker/image_picker_platform_interface +- in_app_purchase/in_app_purchase +- in_app_purchase/in_app_purchase_android +- in_app_purchase/in_app_purchase_storekit - ios_platform_images - local_auth -- video_player +- video_player/video_player +- video_player/video_player_platform_interface +- video_player/video_player_web From 537cc437c0bf4d502654113bba7d941a9266f4a8 Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Fri, 10 Dec 2021 11:03:04 +1300 Subject: [PATCH 240/259] [path_provider] Use the application ID in the application support path (#2845) (#3077) Use the existing executable named directory if it exists, to allow backwards compatibility to work. --- .../path_provider_linux/CHANGELOG.md | 5 + .../lib/path_provider_linux.dart | 59 +----------- .../lib/src/get_application_id.dart | 9 ++ .../lib/src/get_application_id_real.dart | 42 +++++++++ .../lib/src/get_application_id_stub.dart | 6 ++ .../lib/src/path_provider_linux.dart | 92 +++++++++++++++++++ .../path_provider_linux/pubspec.yaml | 3 +- .../test/path_provider_linux_test.dart | 17 +++- 8 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 packages/path_provider/path_provider_linux/lib/src/get_application_id.dart create mode 100644 packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart create mode 100644 packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart create mode 100644 packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 209fddbd61bd..f227570bdd1d 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.3 + +* Change getApplicationSupportPath from using executable name to application ID (if provided). + * If the executable name based directory exists, continue to use that so existing applications continue with the same behaviour. + ## 2.1.2 * Fixes link in README. diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart index ab18db69ddfb..e32af1bf5f13 100644 --- a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart +++ b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart @@ -2,61 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -import 'package:xdg_directories/xdg_directories.dart' as xdg; - -/// The linux implementation of [PathProviderPlatform] -/// -/// This class implements the `package:path_provider` functionality for linux -class PathProviderLinux extends PathProviderPlatform { - /// Constructs an instance of [PathProviderLinux] - PathProviderLinux() : _environment = Platform.environment; - - /// Constructs an instance of [PathProviderLinux] with the given [environment] - @visibleForTesting - PathProviderLinux.private({ - required Map environment, - }) : _environment = environment; - - final Map _environment; - - /// Registers this class as the default instance of [PathProviderPlatform] - static void registerWith() { - PathProviderPlatform.instance = PathProviderLinux(); - } - - @override - Future getTemporaryPath() { - final String environmentTmpDir = _environment['TMPDIR'] ?? ''; - return Future.value( - environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, - ); - } - - @override - Future getApplicationSupportPath() async { - final String processName = path.basenameWithoutExtension( - await File('/proc/self/exe').resolveSymbolicLinks()); - final Directory directory = - Directory(path.join(xdg.dataHome.path, processName)); - // Creating the directory if it doesn't exist, because mobile implementations assume the directory exists - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - return directory.path; - } - - @override - Future getApplicationDocumentsPath() { - return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); - } - - @override - Future getDownloadsPath() { - return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); - } -} +export 'src/path_provider_linux.dart'; diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id.dart new file mode 100644 index 000000000000..e169c025eef1 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id.dart @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// getApplicationId() is implemented using FFI; export a stub for platforms +// that don't support FFI (e.g., web) to avoid having transitive dependencies +// break web compilation. +export 'get_application_id_stub.dart' + if (dart.library.ffi) 'get_application_id_real.dart'; diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart new file mode 100644 index 000000000000..f6d25bbe783e --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +// GApplication* g_application_get_default(); +typedef _GApplicationGetDefaultC = IntPtr Function(); +typedef _GApplicationGetDefaultDart = int Function(); + +// const gchar* g_application_get_application_id(GApplication* application); +typedef _GApplicationGetApplicationIdC = Pointer Function(IntPtr); +typedef _GApplicationGetApplicationIdDart = Pointer Function(int); + +/// Gets the application ID for this app. +String? getApplicationId() { + DynamicLibrary gio; + try { + gio = DynamicLibrary.open('libgio-2.0.so'); + } on ArgumentError { + return null; + } + final _GApplicationGetDefaultDart gApplicationGetDefault = + gio.lookupFunction<_GApplicationGetDefaultC, _GApplicationGetDefaultDart>( + 'g_application_get_default'); + final int app = gApplicationGetDefault(); + if (app == 0) { + return null; + } + + final _GApplicationGetApplicationIdDart gApplicationGetApplicationId = + gio.lookupFunction<_GApplicationGetApplicationIdC, + _GApplicationGetApplicationIdDart>( + 'g_application_get_application_id'); + final Pointer appId = gApplicationGetApplicationId(app); + if (appId == null) { + return null; + } + + return appId.toDartString(); +} diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart new file mode 100644 index 000000000000..909997693626 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart @@ -0,0 +1,6 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Gets the application ID for this app. +String? getApplicationId() => null; diff --git a/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart new file mode 100644 index 000000000000..1544dcea2984 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; + +import 'get_application_id.dart'; + +/// The linux implementation of [PathProviderPlatform] +/// +/// This class implements the `package:path_provider` functionality for Linux. +class PathProviderLinux extends PathProviderPlatform { + /// Constructs an instance of [PathProviderLinux] + PathProviderLinux() : _environment = Platform.environment; + + /// Constructs an instance of [PathProviderLinux] with the given [environment] + @visibleForTesting + PathProviderLinux.private( + {Map environment = const {}, + String? executableName, + String? applicationId}) + : _environment = environment, + _executableName = executableName, + _applicationId = applicationId; + + final Map _environment; + String? _executableName; + String? _applicationId; + + /// Registers this class as the default instance of [PathProviderPlatform] + static void registerWith() { + PathProviderPlatform.instance = PathProviderLinux(); + } + + @override + Future getTemporaryPath() { + final String environmentTmpDir = _environment['TMPDIR'] ?? ''; + return Future.value( + environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, + ); + } + + @override + Future getApplicationSupportPath() async { + final Directory directory = + Directory(path.join(xdg.dataHome.path, await _getId())); + if (directory.existsSync()) { + return directory.path; + } + + // This plugin originally used the executable name as a directory. + // Use that if it exists for backwards compatibility. + final Directory legacyDirectory = + Directory(path.join(xdg.dataHome.path, await _getExecutableName())); + if (legacyDirectory.existsSync()) { + return legacyDirectory.path; + } + + // Create the directory, because mobile implementations assume the directory exists. + await directory.create(recursive: true); + return directory.path; + } + + @override + Future getApplicationDocumentsPath() { + return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); + } + + @override + Future getDownloadsPath() { + return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); + } + + // Gets the name of this executable. + Future _getExecutableName() async { + _executableName ??= path.basenameWithoutExtension( + await File('/proc/self/exe').resolveSymbolicLinks()); + return _executableName!; + } + + // Gets the unique ID for this application. + Future _getId() async { + _applicationId ??= getApplicationId(); + // If no application ID then fall back to using the executable name. + return _applicationId ?? await _getExecutableName(); + } +} diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 67638cdef332..25d1af9936c8 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -16,6 +16,7 @@ flutter: dartPluginClass: PathProviderLinux dependencies: + ffi: ^1.1.2 flutter: sdk: flutter path: ^1.8.0 diff --git a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart index 6dd35000a8ea..1f567c00513d 100644 --- a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart +++ b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -35,8 +36,20 @@ void main() { }); test('getApplicationSupportPath', () async { - final PathProviderPlatform plugin = PathProviderPlatform.instance; - expect(await plugin.getApplicationSupportPath(), startsWith('/')); + final PathProviderPlatform plugin = PathProviderLinux.private( + executableName: 'path_provider_linux_test_binary', + applicationId: 'com.example.Test'); + // Note this will fail if ${xdg.dataHome.path}/path_provider_linux_test_binary exists on the local filesystem. + expect(await plugin.getApplicationSupportPath(), + '${xdg.dataHome.path}/com.example.Test'); + }); + + test('getApplicationSupportPath uses executable name if no application Id', + () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + executableName: 'path_provider_linux_test_binary'); + expect(await plugin.getApplicationSupportPath(), + '${xdg.dataHome.path}/path_provider_linux_test_binary'); }); test('getApplicationDocumentsPath', () async { From e74df9bbef32563ec0326896382a0cd5bba43cad Mon Sep 17 00:00:00 2001 From: Alan Ou <50572272+HiIamAlanOu@users.noreply.github.com> Date: Fri, 10 Dec 2021 19:16:42 +0800 Subject: [PATCH 241/259] [in_app_purchase] Fix upgrading subscription by deferred proration mode on android. (#4560) This PR is for updating the bug of upgrading subscription by deferred mode on android, the purchaseStream of PurchaseStatus will return error. Fixes [flutter/flutter#94439] --- .../in_app_purchase_android/CHANGELOG.md | 4 ++ .../src/in_app_purchase_android_platform.dart | 10 +++-- .../in_app_purchase_android/pubspec.yaml | 2 +- ...in_app_purchase_android_platform_test.dart | 45 +++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) 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 9bce54c2df0f..5c34de267737 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,7 @@ +## 0.2.2 + +* Fixes the `purchaseStream` incorrectly reporting `PurchaseStatus.error` when user upgrades subscription by deferred proration mode. + ## 0.2.1 * Deprecated the `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` method and `InAppPurchaseAndroidPlatformAddition.enablePendingPurchase` property. Since Google Play no longer accepts App submissions that don't support pending purchases it is no longer necessary to acknowledge this through code. 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 7deec6e6ab69..7c8ca529c0f5 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 @@ -270,13 +270,17 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { if (purchases.isNotEmpty) { return Future.wait(purchases); } else { + PurchaseStatus status = PurchaseStatus.error; + if (resultWrapper.responseCode == BillingResponse.userCanceled) { + status = PurchaseStatus.canceled; + } else if (resultWrapper.responseCode == BillingResponse.ok) { + status = PurchaseStatus.purchased; + } return [ PurchaseDetails( purchaseID: '', productID: '', - status: resultWrapper.responseCode == BillingResponse.userCanceled - ? PurchaseStatus.canceled - : PurchaseStatus.error, + status: status, transactionDate: null, verificationData: PurchaseVerificationData( localVerificationData: '', 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 63bfa134377f..4d8864357b57 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.2.1 +version: 0.2.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index 9f00bc79f52a..9c167e5ea3be 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -682,6 +682,51 @@ void main() { GooglePlayPurchaseDetails result = await completer.future; expect(result.status, PurchaseStatus.canceled); }); + + test( + 'should get purchased purchase status when upgrading subscription by deferred proration mode', + () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(sentCode), + 'purchasesList': [] + }); + iapAndroidPlatform.billingClient.callHandler(call); + }); + + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + Stream purchaseStream = iapAndroidPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = purchaseStream.listen((_) { + purchaseDetails = _.first; + completer.complete(purchaseDetails); + subscription.cancel(); + }, onDone: () {}); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + applicationUserName: accountId, + changeSubscriptionParam: ChangeSubscriptionParam( + oldPurchaseDetails: GooglePlayPurchaseDetails.fromPurchase( + dummyUnacknowledgedPurchase), + prorationMode: ProrationMode.deferred, + )); + await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + PurchaseDetails result = await completer.future; + expect(result.status, PurchaseStatus.purchased); + }); }); group('complete purchase', () { From f9f6597ad844f5d5c1d76ecbf8576da5e7f8ba5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Uek=C3=B6tter?= Date: Fri, 10 Dec 2021 12:17:17 +0100 Subject: [PATCH 242/259] [shared_preferences] Fix example in readme for mocking values (#4411) This fixes the example for mocking values in the readme of shared_preferences. After the NNBD migration mock values can only be of type Object. --- packages/shared_preferences/shared_preferences/CHANGELOG.md | 4 ++++ packages/shared_preferences/shared_preferences/README.md | 3 ++- packages/shared_preferences/shared_preferences/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index cc29dd98fc89..d8c4331133aa 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.11 + +* Corrects example for mocking in readme. + ## 2.0.10 * Removes obsolete manual registration of Windows and Linux implementations. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index e51ddea1c890..46d022f4647c 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -42,5 +42,6 @@ _incrementCounter() async { You can populate `SharedPreferences` with initial values in your tests by running this code: ```dart -SharedPreferences.setMockInitialValues (Map values); +Map values = {'counter': 1}; +SharedPreferences.setMockInitialValues(values); ``` diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 156e2e230565..a16cd2009738 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.10 +version: 2.0.11 environment: sdk: ">=2.14.0 <3.0.0" From 684c34f5692808ce2fbf9a3d89290d3d7a4f7e33 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Fri, 10 Dec 2021 03:18:07 -0800 Subject: [PATCH 243/259] Add missing return for nullably typed function (#4598) --- script/tool/lib/src/pubspec_check_command.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 04c3848a7dec..4f2b208524d9 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -211,6 +211,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { return '"description" is too long. pub.dev recommends package ' 'descriptions of 60-180 characters.'; } + return null; } bool _checkIssueLink(Pubspec pubspec) { From b78018a365046629be142a429712c405896ff5a8 Mon Sep 17 00:00:00 2001 From: Daewon Kim Date: Fri, 10 Dec 2021 06:20:58 -0500 Subject: [PATCH 244/259] [in_app-purchase] Updated the ReadMe to remove the deprecated `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases ` method (#4597) This will resolve flutter/flutter#93837 & this work is just the follow-up from #4527 PR. We need to update the documentation in the README.md of the in_app_purchase package to remove the instruction of initialization with the deprecated `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases` method. Fixes https://github.com/flutter/flutter/issues/94977 --- .../in_app_purchase/CHANGELOG.md | 4 ++++ .../in_app_purchase/in_app_purchase/README.md | 22 ------------------- .../in_app_purchase/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 9e82ebdfe7a8..232dacf0b71e 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + +* Removes the instructions on initializing the plugin since this functionality is deprecated. + ## 2.0.0 * **BREAKING CHANGES**: diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index f3ed1e0570f7..b67f9e5b52ac 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -54,7 +54,6 @@ See also the codelab for [in-app purchases in Flutter](https://codelabs.develope This section has examples of code for the following tasks: -* [Initializing the plugin](#initializing-the-plugin) * [Listening to purchase updates](#listening-to-purchase-updates) * [Connecting to the underlying store](#connecting-to-the-underlying-store) * [Loading products for sale](#loading-products-for-sale) @@ -65,27 +64,6 @@ This section has examples of code for the following tasks: * [Accessing platform specific product or purchase properties](#accessing-platform-specific-product-or-purchase-properties) * [Presenting a code redemption sheet (iOS 14)](#presenting-a-code-redemption-sheet-ios-14) -### Initializing the plugin - -The following initialization code is required for Google Play: - -```dart -// Import `in_app_purchase_android.dart` to be able to access the -// `InAppPurchaseAndroidPlatformAddition` class. -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:flutter/foundation.dart'; - -void main() { - // Inform the plugin that this app supports pending purchases on Android. - // An error will occur on Android if you access the plugin `instance` - // without this call. - if (defaultTargetPlatform == TargetPlatform.android) { - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); - } - runApp(MyApp()); -} -``` - **Note:** It is not necessary to depend on `com.android.billingclient:billing` in your own app's `android/app/build.gradle` file. If you choose to do so know that conflicts might occur. ### Listening to purchase updates diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 72fac1d29f55..c7bb11f38f8f 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 2.0.0 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" From e7dbb5f4a59fc13514183b278be50dd5b6b24eb7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 10 Dec 2021 14:33:10 -0500 Subject: [PATCH 245/259] [path_provider] Fix handling of null application ID (#4606) The lookup of application ID was handling `null` (Dart null), but not `nullptr` (Dart representation of a C null pointer), so was throwing an exception in applications without an application ID. This includes the `shared_preferences_linux` example application, so this fixes tree breakage. --- .../path_provider_linux/CHANGELOG.md | 5 ++ .../lib/src/get_application_id_real.dart | 68 ++++++++++++++----- .../path_provider_linux/pubspec.yaml | 3 +- .../test/get_application_id_test.dart | 62 +++++++++++++++++ 4 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 packages/path_provider/path_provider_linux/test/get_application_id_test.dart diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index f227570bdd1d..cfea5702dbe1 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.4 + +* Fixes `getApplicationSupportPath` handling of applications where the + application ID is not set. + ## 2.1.3 * Change getApplicationSupportPath from using executable name to application ID (if provided). diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart index f6d25bbe783e..d2e60fb6ff82 100644 --- a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart @@ -4,6 +4,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; // GApplication* g_application_get_default(); typedef _GApplicationGetDefaultC = IntPtr Function(); @@ -13,30 +14,65 @@ typedef _GApplicationGetDefaultDart = int Function(); typedef _GApplicationGetApplicationIdC = Pointer Function(IntPtr); typedef _GApplicationGetApplicationIdDart = Pointer Function(int); +/// Interface for interacting with libgio. +@visibleForTesting +class GioUtils { + /// Creates a default instance that uses the real libgio. + GioUtils() { + try { + _gio = DynamicLibrary.open('libgio-2.0.so'); + } on ArgumentError { + _gio = null; + } + } + + DynamicLibrary? _gio; + + /// True if libgio was opened successfully. + bool get libraryIsPresent => _gio != null; + + /// Wraps `g_application_get_default`. + int gApplicationGetDefault() { + if (_gio == null) { + return 0; + } + final _GApplicationGetDefaultDart getDefault = _gio! + .lookupFunction<_GApplicationGetDefaultC, _GApplicationGetDefaultDart>( + 'g_application_get_default'); + return getDefault(); + } + + /// Wraps g_application_get_application_id. + Pointer gApplicationGetApplicationId(int app) { + if (_gio == null) { + return nullptr; + } + final _GApplicationGetApplicationIdDart gApplicationGetApplicationId = _gio! + .lookupFunction<_GApplicationGetApplicationIdC, + _GApplicationGetApplicationIdDart>( + 'g_application_get_application_id'); + return gApplicationGetApplicationId(app); + } +} + +/// Allows overriding the default GioUtils instance with a fake for testing. +@visibleForTesting +GioUtils? gioUtilsOverride; + /// Gets the application ID for this app. String? getApplicationId() { - DynamicLibrary gio; - try { - gio = DynamicLibrary.open('libgio-2.0.so'); - } on ArgumentError { + final GioUtils gio = gioUtilsOverride ?? GioUtils(); + if (!gio.libraryIsPresent) { return null; } - final _GApplicationGetDefaultDart gApplicationGetDefault = - gio.lookupFunction<_GApplicationGetDefaultC, _GApplicationGetDefaultDart>( - 'g_application_get_default'); - final int app = gApplicationGetDefault(); + + final int app = gio.gApplicationGetDefault(); if (app == 0) { return null; } - - final _GApplicationGetApplicationIdDart gApplicationGetApplicationId = - gio.lookupFunction<_GApplicationGetApplicationIdC, - _GApplicationGetApplicationIdDart>( - 'g_application_get_application_id'); - final Pointer appId = gApplicationGetApplicationId(app); - if (appId == null) { + final Pointer appId = gio.gApplicationGetApplicationId(app); + if (appId == null || appId == nullptr) { return null; } - return appId.toDartString(); } diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 25d1af9936c8..ec1d8d8b3148 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.3 +version: 2.1.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -19,6 +19,7 @@ dependencies: ffi: ^1.1.2 flutter: sdk: flutter + meta: ^1.3.0 path: ^1.8.0 path_provider_platform_interface: ^2.0.0 xdg_directories: ^0.2.0 diff --git a/packages/path_provider/path_provider_linux/test/get_application_id_test.dart b/packages/path_provider/path_provider_linux/test/get_application_id_test.dart new file mode 100644 index 000000000000..d9eb5163b5fe --- /dev/null +++ b/packages/path_provider/path_provider_linux/test/get_application_id_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_linux/src/get_application_id_real.dart'; + +class _FakeGioUtils implements GioUtils { + int? application; + Pointer? applicationId; + + @override + bool libraryIsPresent = false; + + @override + int gApplicationGetDefault() => application!; + + @override + Pointer gApplicationGetApplicationId(int app) => applicationId!; +} + +void main() { + late _FakeGioUtils fakeGio; + + setUp(() { + fakeGio = _FakeGioUtils(); + gioUtilsOverride = fakeGio; + }); + + tearDown(() { + gioUtilsOverride = null; + }); + + test('returns null if libgio is not available', () { + expect(getApplicationId(), null); + }); + + test('returns null if g_paplication_get_default returns 0', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 0; + expect(getApplicationId(), null); + }); + + test('returns null if g_application_get_application_id returns nullptr', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 1; + fakeGio.applicationId = nullptr; + expect(getApplicationId(), null); + }); + + test('returns value if g_application_get_application_id returns a value', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 1; + const String id = 'foo'; + final Pointer idPtr = id.toNativeUtf8(); + fakeGio.applicationId = idPtr; + expect(getApplicationId(), id); + calloc.free(idPtr); + }); +} From a5b664db7c7b053328202a20830dcdcc4010353c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 10 Dec 2021 16:09:20 -0800 Subject: [PATCH 246/259] [tools] fix typo in tools (#4607) --- script/tool/lib/src/lint_android_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index a7b5c4f2e8bf..49b2181a4615 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -12,9 +12,9 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -/// Lint the CocoaPod podspecs and run unit tests. +/// Run 'gradlew lint'. /// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +/// See https://developer.android.com/studio/write/lint. class LintAndroidCommand extends PackageLoopingCommand { /// Creates an instance of the linter command. LintAndroidCommand( From 1a56bb2daea752887320d8a3d5eb93af52afc6a4 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 13 Dec 2021 16:09:40 -0500 Subject: [PATCH 247/259] [local_auth] Switch to new analysis options (#4604) Removes the legacy analysis options and fixes all violations. Most fixes were automated `dart fix --apply` changes. Part of https://github.com/flutter/flutter/issues/76229 --- packages/local_auth/CHANGELOG.md | 3 +- packages/local_auth/analysis_options.yaml | 1 - packages/local_auth/example/lib/main.dart | 105 ++++++++++-------- packages/local_auth/example/pubspec.yaml | 4 +- packages/local_auth/lib/local_auth.dart | 8 +- packages/local_auth/pubspec.yaml | 2 +- packages/local_auth/test/local_auth_test.dart | 4 +- script/configs/custom_analysis.yaml | 1 - 8 files changed, 68 insertions(+), 60 deletions(-) delete mode 100644 packages/local_auth/analysis_options.yaml diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index 82358d61e644..7fe3a98ed08f 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 1.1.9 +* Updates code for analysis option changes. * Updates Android compileSdkVersion to 31. ## 1.1.8 diff --git a/packages/local_auth/analysis_options.yaml b/packages/local_auth/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/local_auth/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart index b6b6f3278423..a9604b3411b7 100644 --- a/packages/local_auth/example/lib/main.dart +++ b/packages/local_auth/example/lib/main.dart @@ -31,7 +31,7 @@ class _MyAppState extends State { void initState() { super.initState(); auth.isDeviceSupported().then( - (isSupported) => setState(() => _supportState = isSupported + (bool isSupported) => setState(() => _supportState = isSupported ? _SupportState.supported : _SupportState.unsupported), ); @@ -45,7 +45,9 @@ class _MyAppState extends State { canCheckBiometrics = false; print(e); } - if (!mounted) return; + if (!mounted) { + return; + } setState(() { _canCheckBiometrics = canCheckBiometrics; @@ -60,7 +62,9 @@ class _MyAppState extends State { availableBiometrics = []; print(e); } - if (!mounted) return; + if (!mounted) { + return; + } setState(() { _availableBiometrics = availableBiometrics; @@ -85,11 +89,13 @@ class _MyAppState extends State { print(e); setState(() { _isAuthenticating = false; - _authorized = "Error - ${e.message}"; + _authorized = 'Error - ${e.message}'; }); return; } - if (!mounted) return; + if (!mounted) { + return; + } setState( () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); @@ -116,11 +122,13 @@ class _MyAppState extends State { print(e); setState(() { _isAuthenticating = false; - _authorized = "Error - ${e.message}"; + _authorized = 'Error - ${e.message}'; }); return; } - if (!mounted) return; + if (!mounted) { + return; + } final String message = authenticated ? 'Authorized' : 'Not Authorized'; setState(() { @@ -128,7 +136,7 @@ class _MyAppState extends State { }); } - void _cancelAuthentication() async { + Future _cancelAuthentication() async { await auth.stopAuthentication(); setState(() => _isAuthenticating = false); } @@ -142,67 +150,68 @@ class _MyAppState extends State { ), body: ListView( padding: const EdgeInsets.only(top: 30), - children: [ + children: [ Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ if (_supportState == _SupportState.unknown) - CircularProgressIndicator() + const CircularProgressIndicator() else if (_supportState == _SupportState.supported) - Text("This device is supported") + const Text('This device is supported') else - Text("This device is not supported"), - Divider(height: 100), + const Text('This device is not supported'), + const Divider(height: 100), Text('Can check biometrics: $_canCheckBiometrics\n'), ElevatedButton( child: const Text('Check biometrics'), onPressed: _checkBiometrics, ), - Divider(height: 100), + const Divider(height: 100), Text('Available biometrics: $_availableBiometrics\n'), ElevatedButton( child: const Text('Get available biometrics'), onPressed: _getAvailableBiometrics, ), - Divider(height: 100), + const Divider(height: 100), Text('Current State: $_authorized\n'), - (_isAuthenticating) - ? ElevatedButton( - onPressed: _cancelAuthentication, + 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("Cancel Authentication"), - Icon(Icons.cancel), + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), ], ), - ) - : Column( - children: [ - ElevatedButton( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Authenticate'), - Icon(Icons.perm_device_information), - ], - ), - onPressed: _authenticate, - ), - ElevatedButton( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_isAuthenticating - ? 'Cancel' - : 'Authenticate: biometrics only'), - Icon(Icons.fingerprint), - ], - ), - onPressed: _authenticateWithBiometrics, - ), - ], + onPressed: _authenticateWithBiometrics, ), + ], + ), ], ), ], diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index 3aa8fd848057..ad0de2002a4c 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -18,10 +18,10 @@ dependencies: path: ../ dev_dependencies: - integration_test: - sdk: flutter flutter_driver: sdk: flutter + integration_test: + sdk: flutter pedantic: ^1.10.0 flutter: diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart index 0b75a83d4029..af02d57a7359 100644 --- a/packages/local_auth/lib/local_auth.dart +++ b/packages/local_auth/lib/local_auth.dart @@ -32,7 +32,7 @@ void setMockPathProviderPlatform(Platform platform) { class LocalAuthentication { /// The `authenticateWithBiometrics` method has been deprecated. /// Use `authenticate` with `biometricOnly: true` instead - @Deprecated("Use `authenticate` with `biometricOnly: true` instead") + @Deprecated('Use `authenticate` with `biometricOnly: true` instead') Future authenticateWithBiometrics({ required String localizedReason, bool useErrorDialogs = true, @@ -160,9 +160,9 @@ class LocalAuthentication { final List result = (await _channel.invokeListMethod( 'getAvailableBiometrics', )) ?? - []; + []; final List biometrics = []; - result.forEach((String value) { + for (final String value in result) { switch (value) { case 'face': biometrics.add(BiometricType.face); @@ -176,7 +176,7 @@ class LocalAuthentication { case 'undefined': break; } - }); + } return biometrics; } } diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index 4f5ef26d9fb1..9303b0c8cc74 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/master/packages/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.8 +version: 1.1.9 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/local_auth/test/local_auth_test.dart b/packages/local_auth/test/local_auth_test.dart index b24de8bd3c11..758b9cec0e97 100644 --- a/packages/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/test/local_auth_test.dart @@ -30,7 +30,7 @@ void main() { log.clear(); }); - group("With device auth fail over", () { + group('With device auth fail over', () { test('authenticate with no args on Android.', () async { setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); await localAuthentication.authenticate( @@ -108,7 +108,7 @@ void main() { }); }); - group("With biometrics only", () { + group('With biometrics only', () { test('authenticate with no args on Android.', () async { setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); await localAuthentication.authenticate( diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index e991e5e1446c..71ce1417196f 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -27,7 +27,6 @@ - in_app_purchase/in_app_purchase_android - in_app_purchase/in_app_purchase_storekit - ios_platform_images -- local_auth - video_player/video_player - video_player/video_player_platform_interface - video_player/video_player_web From bfa34380ea05631aa21997f4908f74eda65f651e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 13 Dec 2021 17:20:35 -0500 Subject: [PATCH 248/259] [video_player] Fix a flaky test (#4611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'reports buffering status' test was written in an unnecessary complicated way that used pairs of completers and booleans that tracked the same thing, and more importantly had a bug (hidden by the fact that this package is still on legacy analysis options) where the `await` statements weren't actually doing anything, because they were not actually `await`ing futures. This fixes the lack of awaits—which should fix the flake—and removes the unnecessary booleans. Fixes https://github.com/flutter/flutter/issues/94775 --- packages/video_player/video_player/CHANGELOG.md | 1 + .../integration_test/video_player_test.dart | 17 +++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f9031685b583..9d4e36d72017 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -2,6 +2,7 @@ * Fixes integration tests. * Updates Android compileSdkVersion to 31. +* Fixes a flaky integration test. ## 2.2.7 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 866b5bce0a8d..63a38290a613 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 @@ -48,17 +48,13 @@ void main() { await networkController.setVolume(0); final Completer started = Completer(); final Completer ended = Completer(); - bool startedBuffering = false; - bool endedBuffering = false; networkController.addListener(() { - if (networkController.value.isBuffering && !startedBuffering) { - startedBuffering = true; + if (!started.isCompleted && networkController.value.isBuffering) { started.complete(); } - if (startedBuffering && + if (started.isCompleted && !networkController.value.isBuffering && - !endedBuffering) { - endedBuffering = true; + !ended.isCompleted) { ended.complete(); } }); @@ -72,11 +68,8 @@ void main() { expect(networkController.value.position, (Duration position) => position > const Duration(seconds: 0)); - await started; - expect(startedBuffering, true); - - await ended; - expect(endedBuffering, true); + await expectLater(started.future, completes); + await expectLater(ended.future, completes); }, skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), ); From 453089d1fa9b45e17352807c6e73adc059a027c2 Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Tue, 14 Dec 2021 16:56:10 -0800 Subject: [PATCH 249/259] update firebase test key (#4615) --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 95bd6092674b..5dff2d1b035f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -195,7 +195,7 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!c9446a7b11d5520c2ebce3c64ccc82fe6d146272cb06a4a4590e22c389f33153f951347a25422522df1a81fe2f085e9a!] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!cc769765170bebc37e0556e2da5915ca64ee37f4ec8c966ec147e2f59578b476c99e457eafce4e2f8b1a4e305f7096b8!] build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. From 910055b2b53259efef8c76a50f5ae2efa217c270 Mon Sep 17 00:00:00 2001 From: keyonghan <54558023+keyonghan@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:26:33 -0800 Subject: [PATCH 250/259] update firebase key (#4616) --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 5dff2d1b035f..bcd0e49327a3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -195,7 +195,7 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!cc769765170bebc37e0556e2da5915ca64ee37f4ec8c966ec147e2f59578b476c99e457eafce4e2f8b1a4e305f7096b8!] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[de02374f8d2d14d50792c6b521af2dfb86cbb522efed104f905002e4332546104d387d2bb8710956b729b4bd6533bba0] build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. From 92539fc24765acb755bcd942ea598c1c4aa01b56 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 15 Dec 2021 11:22:52 -0500 Subject: [PATCH 251/259] [flutter_plugin_tools] Auto-retry failed FTL tests (#4610) Currently the flake situation for Firebase Test Lab tests is very bad, and the task running those tests are some of the slowest tasks in the CI. Re-running failed tests is slow, manual work that is signficantly affecting productivity. There are plans to actually address the flake in the short-to-medium term, but in the immediate term this will improve the CI situation, as well as reducing the drag on engineering time that could be spent on the root causes. Part of https://github.com/flutter/flutter/issues/95063 --- script/tool/CHANGELOG.md | 2 + .../lib/src/firebase_test_lab_command.dart | 74 +++++++++++++------ .../test/firebase_test_lab_command_test.dart | 41 +++++++++- 3 files changed, 93 insertions(+), 24 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 72539c24c5fc..12ccf17d4d0b 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -13,6 +13,8 @@ - Fix `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). +- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch + for flake issues. ## 0.7.3 diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4e53ee8fbace..e824d8ad1a90 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -176,30 +176,22 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String testRunId = getStringArg('test-run-id'); final String resultsDir = 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - ]; - for (final String device in getStringListArg('device')) { - args.addAll(['--device', device]); - } - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); - if (exitCode != 0) { - printError('Test failure for $testName'); + // Automatically retry failures; there is significant flake with these + // tests whose cause isn't yet understood, and having to re-run the + // entire shard for a flake in any one test is extremely slow. This should + // be removed once the root cause of the flake is understood. + // See https://github.com/flutter/flutter/issues/95063 + const int maxRetries = 2; + bool passing = false; + for (int i = 1; i <= maxRetries && !passing; ++i) { + if (i > 1) { + logWarning('$testName failed on attempt ${i - 1}. Retrying...'); + } + passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); + } + if (!passing) { + printError('Test failure for $testName after $maxRetries attempts'); errors.add('$testName failed tests'); } } @@ -238,6 +230,42 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } + /// Runs [test] from [example] as a Firebase Test Lab test, returning true if + /// the test passed. + /// + /// [resultsDir] should be a unique-to-the-test-run directory to store the + /// results on the server. + Future _runFirebaseTest( + RepositoryPackage example, + File test, { + required String resultsDir, + }) async { + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '7m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + for (final String device in getStringListArg('device')) ...[ + '--device', + device + ], + ]; + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: example.directory); + + return exitCode == 0; + } + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is /// already configured. /// diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 65f398b32ca8..1dfd8ba66b58 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -271,7 +271,7 @@ public class MainActivityTest { ); }); - test('fails if a test fails', () async { + test('fails if a test fails twice', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; final Directory pluginDir = @@ -287,6 +287,7 @@ public class MainActivityTest { MockProcess(), // auth MockProcess(), // config MockProcess(exitCode: 1), // integration test #1 + MockProcess(exitCode: 1), // integration test #1 retry MockProcess(), // integration test #2 ]; @@ -315,6 +316,44 @@ public class MainActivityTest { ); }); + test('passes with warning if a test fails once, then passes on retry', + () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #1 retry + MockProcess(), // integration test #2 + ]; + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ]); + + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('bar_test.dart failed on attempt 1. Retrying...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', From ee97b6534dd575c0df517d7bcffc89a6db43b3ed Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 16 Dec 2021 16:29:20 -0500 Subject: [PATCH 252/259] [video_player] Eliminate platform channel from mock platform (#4588) --- .../video_player/video_player/CHANGELOG.md | 5 +- .../video_player/lib/video_player.dart | 16 +- .../video_player/video_player/pubspec.yaml | 8 +- .../video_player_initialization_test.dart | 2 + .../video_player/test/video_player_test.dart | 215 ++++++------------ 5 files changed, 89 insertions(+), 157 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 9d4e36d72017..6f2a2b7250d0 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.2.8 +* Changes the way the `VideoPlayerPlatform` instance is cached in the + controller, so that it's no longer impossible to change after the first use. +* Updates unit tests to be self-contained. * Fixes integration tests. * Updates Android compileSdkVersion to 31. * Fixes a flaky integration test. diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index ff727f1432c8..523a1adc5425 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -17,10 +17,18 @@ export 'package:video_player_platform_interface/video_player_platform_interface. import 'src/closed_caption_file.dart'; export 'src/closed_caption_file.dart'; -final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance - // This will clear all open videos on the platform when a full restart is - // performed. - ..init(); +VideoPlayerPlatform? _lastVideoPlayerPlatform; + +VideoPlayerPlatform get _videoPlayerPlatform { + VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance; + if (_lastVideoPlayerPlatform != currentInstance) { + // This will clear all open videos on the platform when a full restart is + // performed. + currentInstance.init(); + _lastVideoPlayerPlatform = currentInstance; + } + return currentInstance; +} /// The duration, current position, buffering state, error state and settings /// of a [VideoPlayerController]. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index f0d5951b1403..f8c091786526 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.7 +version: 2.2.8 environment: sdk: ">=2.14.0 <3.0.0" @@ -25,12 +25,6 @@ dependencies: sdk: flutter meta: ^1.3.0 video_player_platform_interface: ^4.2.0 - # The design on https://flutter.dev/go/federated-plugins was to leave - # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. The exact value doesn't matter since - # the constraints on the interface pins it. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 video_player_web: ^2.0.0 html: ^0.15.0 diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 13bfd7be7889..1870934a931e 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'video_player_test.dart' show FakeVideoPlayerPlatform; @@ -13,6 +14,7 @@ void main() { test('plugin initialized', () async { TestWidgetsFlutterBinding.ensureInitialized(); FakeVideoPlayerPlatform fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', 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 08fd9dc604f1..959f98f25e28 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -11,8 +11,6 @@ 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/messages.dart'; -import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class FakeController extends ValueNotifier @@ -187,6 +185,7 @@ void main() { setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; }); group('initialize', () { @@ -196,10 +195,8 @@ void main() { ); await controller.initialize(); - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].asset, 'a.avi'); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].packageName, - null); + expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].package, null); }); test('network', () async { @@ -209,15 +206,15 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}, ); }); @@ -230,16 +227,16 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, - 'dash', + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, - {}, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, ); }); @@ -251,15 +248,15 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {'Authorization': 'Bearer token'}, ); }); @@ -268,15 +265,12 @@ void main() { final VideoPlayerController controller = VideoPlayerController.network( 'http://testing.com/invalid_url', ); - try { - late dynamic error; - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) => error = e); - final PlatformException platformEx = error; - expect(platformEx.code, equals('VideoError')); - } finally { - fakeVideoPlayerPlatform.forceInitError = false; - } + + late dynamic error; + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) => error = e); + final PlatformException platformEx = error; + expect(platformEx.code, equals('VideoError')); }); test('file', () async { @@ -284,8 +278,7 @@ void main() { VideoPlayerController.file(File('a.avi')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'file://a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'file://a.avi'); }); }); @@ -294,8 +287,7 @@ void main() { VideoPlayerController.contentUri(Uri.parse('content://video')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'content://video'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); }); test('dispose', () async { @@ -571,11 +563,11 @@ void main() { expect(controller.value.isPlaying, isFalse); await controller.play(); expect(controller.value.isPlaying, isTrue); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'completed'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.completed)); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -589,30 +581,30 @@ void main() { await controller.initialize(); expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'bufferingStart'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingStart)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); const Duration bufferStart = Duration(seconds: 0); const Duration bufferEnd = Duration(milliseconds: 500); - fakeVideoEventStream.eventsChannel.sendEvent({ - 'event': 'bufferingUpdate', - 'values': >[ - [bufferStart.inMilliseconds, bufferEnd.inMilliseconds] - ], - }); + fakeVideoEventStream + ..add(VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(bufferStart, bufferEnd), + ])); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); expect(controller.value.buffered.length, 1); expect(controller.value.buffered[0].toString(), DurationRange(bufferStart, bufferEnd).toString()); - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'bufferingEnd'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); }); @@ -807,155 +799,88 @@ void main() { }); } -class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { - FakeVideoPlayerPlatform() { - TestHostVideoPlayerApi.setup(this); - } - +class FakeVideoPlayerPlatform extends VideoPlayerPlatform { Completer initialized = Completer(); List calls = []; - List dataSourceDescriptions = []; - final Map streams = {}; + List dataSources = []; + final Map> streams = + >{}; bool forceInitError = false; int nextTextureId = 0; final Map _positions = {}; @override - TextureMessage create(CreateMessage arg) { + Future create(DataSource dataSource) async { calls.add('create'); - streams[nextTextureId] = FakeVideoEventStream( - nextTextureId, 100, 100, const Duration(seconds: 1), forceInitError); - TextureMessage result = TextureMessage(); - result.textureId = nextTextureId++; - dataSourceDescriptions.add(arg); - return result; + StreamController stream = StreamController(); + streams[nextTextureId] = stream; + if (forceInitError) { + stream.addError(PlatformException( + code: 'VideoError', message: 'Video player had error XYZ')); + } else { + stream.add(VideoEvent( + eventType: VideoEventType.initialized, + size: Size(100, 100), + duration: Duration(seconds: 1))); + } + dataSources.add(dataSource); + return nextTextureId++; } @override - void dispose(TextureMessage arg) { + Future dispose(int textureId) async { calls.add('dispose'); } @override - void initialize() { + Future init() async { calls.add('init'); initialized.complete(true); } + Stream videoEventsFor(int textureId) { + return streams[textureId]!.stream; + } + @override - void pause(TextureMessage arg) { + Future pause(int textureId) async { calls.add('pause'); } @override - void play(TextureMessage arg) { + Future play(int textureId) async { calls.add('play'); } @override - PositionMessage position(TextureMessage arg) { + Future getPosition(int textureId) async { calls.add('position'); - final Duration position = - _positions[arg.textureId] ?? const Duration(seconds: 0); - return PositionMessage()..position = position.inMilliseconds; + return _positions[textureId] ?? const Duration(seconds: 0); } @override - void seekTo(PositionMessage arg) { + Future seekTo(int textureId, Duration position) async { calls.add('seekTo'); - _positions[arg.textureId!] = Duration(milliseconds: arg.position!); + _positions[textureId] = position; } @override - void setLooping(LoopingMessage arg) { + Future setLooping(int textureId, bool looping) async { calls.add('setLooping'); } @override - void setVolume(VolumeMessage arg) { + Future setVolume(int textureId, double volume) async { calls.add('setVolume'); } @override - void setPlaybackSpeed(PlaybackSpeedMessage arg) { + Future setPlaybackSpeed(int textureId, double speed) async { calls.add('setPlaybackSpeed'); } @override - void setMixWithOthers(MixWithOthersMessage arg) { + Future setMixWithOthers(bool mixWithOthers) async { calls.add('setMixWithOthers'); } } - -class FakeVideoEventStream { - FakeVideoEventStream(this.textureId, this.width, this.height, this.duration, - this.initWithError) { - eventsChannel = FakeEventsChannel( - 'flutter.io/videoPlayer/videoEvents$textureId', onListen); - } - - int textureId; - int width; - int height; - Duration duration; - bool initWithError; - late FakeEventsChannel eventsChannel; - - void onListen() { - if (!initWithError) { - eventsChannel.sendEvent({ - 'event': 'initialized', - 'duration': duration.inMilliseconds, - 'width': width, - 'height': height, - }); - } else { - eventsChannel.sendError('VideoError', 'Video player had error XYZ'); - } - } -} - -class FakeEventsChannel { - FakeEventsChannel(String name, this.onListen) { - eventsMethodChannel = MethodChannel(name); - eventsMethodChannel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel eventsMethodChannel; - VoidCallback onListen; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'listen': - onListen(); - break; - } - return Future.sync(() {}); - } - - void sendEvent(dynamic event) { - _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event)); - } - - void sendError(String code, [String? message, dynamic details]) { - _sendMessage(const StandardMethodCodec().encodeErrorEnvelope( - code: code, - message: message, - details: details, - )); - } - - void _sendMessage(ByteData data) { - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData? data) {}); - } -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; From 73cb8a7c7fa74686cb3fb60cb8688b90d1a73e62 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Fri, 17 Dec 2021 10:54:02 -0500 Subject: [PATCH 253/259] Prevent setting user agent string to default value on every rebuild (#4618) --- .../webview_flutter_android/CHANGELOG.md | 5 ++++ .../lib/webview_android_widget.dart | 7 ++--- .../webview_flutter_android/pubspec.yaml | 2 +- .../test/webview_android_widget_test.dart | 26 +++++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 7bd33879cd7d..1337bab304b8 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.8.1 + +* Fixes bug where the default user agent string was being set for every rebuild. See + https://github.com/flutter/flutter/issues/94847. + ## 2.8.0 * Implements new cookie manager for setting cookies and providing initial cookies. 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 1dec9c105741..de55b5207a84 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 @@ -418,11 +418,12 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { } Future _setUserAgent(WebSetting userAgent) { - if (userAgent.isPresent && userAgent.value != null) { - return webView.settings.setUserAgentString(userAgent.value!); + if (userAgent.isPresent) { + // If the string is empty, the system default value will be used. + return webView.settings.setUserAgentString(userAgent.value ?? ''); } - return webView.settings.setUserAgentString(''); + return Future.value(); } Future _setZoomEnabled(bool zoomEnabled) { diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 34bea570ae43..0b78c72ecc4c 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.8.0 +version: 2.8.1 environment: sdk: ">=2.14.0 <3.0.0" 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 c203ef04a2ce..2b25022f9087 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 @@ -483,6 +483,32 @@ void main() { }); }); + testWidgets('no update to userAgentString when there is no change', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + )); + + verifyNever(mockWebSettings.setUserAgentString(any)); + }); + + testWidgets('update null userAgentString with empty string', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.of(null), + )); + + verify(mockWebSettings.setUserAgentString('')); + }); + testWidgets('currentUrl', (WidgetTester tester) async { await buildWidget(tester); From d383fada852f4924e37d6ee9673950088413bac3 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 17 Dec 2021 17:59:21 +0100 Subject: [PATCH 254/259] [webview_flutter] Enable setAllowFileAccess on Android setting when loading files (#4601) --- .../webview_flutter_android/CHANGELOG.md | 4 +++ .../GeneratedAndroidWebView.java | 33 +++++++++++++++++++ .../WebSettingsHostApiImpl.java | 6 ++++ .../lib/src/android_webview.dart | 10 ++++++ .../lib/src/android_webview.pigeon.dart | 25 ++++++++++++++ .../lib/src/android_webview_api_impls.dart | 11 +++++++ .../lib/webview_android_widget.dart | 1 + .../pigeons/android_webview.dart | 2 ++ .../webview_flutter_android/pubspec.yaml | 2 +- .../test/android_webview.pigeon.dart | 23 +++++++++++++ .../test/android_webview_test.dart | 8 +++++ .../test/android_webview_test.mocks.dart | 8 ++--- .../test/webview_android_widget_test.dart | 9 +++++ .../webview_android_widget_test.mocks.dart | 9 ++--- 14 files changed, 142 insertions(+), 9 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 1337bab304b8..818a13439c95 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.2 + +* Adds the `WebSettings.setAllowFileAccess()` method and ensure that file access is allowed when the `WebViewAndroidWidget.loadFile()` method is executed. + ## 2.8.1 * Fixes bug where the default user agent string was being set for every rebuild. See 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 8ef0b8d11f96..15b78b718115 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 @@ -1152,6 +1152,8 @@ public interface WebSettingsHostApi { void setBuiltInZoomControls(Long instanceId, Boolean enabled); + void setAllowFileAccess(Long instanceId, Boolean enabled); + /** The codec used by WebSettingsHostApi. */ static MessageCodec getCodec() { return WebSettingsHostApiCodec.INSTANCE; @@ -1556,6 +1558,37 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess", + 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."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setAllowFileAccess(instanceIdArg.longValue(), enabledArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java index 239ef473b546..b168e206214f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -118,4 +118,10 @@ public void setBuiltInZoomControls(Long instanceId, Boolean enabled) { final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); webSettings.setBuiltInZoomControls(enabled); } + + @Override + public void setAllowFileAccess(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setAllowFileAccess(enabled); + } } 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 dfa05cd92ee6..10989321a9bb 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 @@ -546,6 +546,16 @@ class WebSettings { Future setBuiltInZoomControls(bool enabled) { return api.setBuiltInZoomControlsFromInstance(this, enabled); } + + /// Enables or disables file access within WebView. + /// + /// This enables or disables file system access only. Assets and resources are + /// still accessible using file:///android_asset and file:///android_res. The + /// default value is true for apps targeting Build.VERSION_CODES.Q and below, + /// and false when targeting Build.VERSION_CODES.R and above. + Future setAllowFileAccess(bool enabled) { + return api.setAllowFileAccessFromInstance(this, enabled); + } } /// Exposes a channel to receive calls from javaScript. 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 810a71732e7b..20391c43d966 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 @@ -1179,6 +1179,31 @@ class WebSettingsHostApi { return; } } + + Future setAllowFileAccess(int arg_instanceId, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', 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.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } } class _JavaScriptChannelHostApiCodec extends StandardMessageCodec { 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 1db5ed449c56..ead60f6a2b35 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 @@ -437,6 +437,17 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { enabled, ); } + + /// Helper method to convert instances ids to objects. + Future setAllowFileAccessFromInstance( + WebSettings instance, + bool enabled, + ) { + return setAllowFileAccess( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } } /// Host api implementation for [JavaScriptChannel]. 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 de55b5207a84..bf85ac97687e 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 @@ -179,6 +179,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { ? absoluteFilePath : 'file://$absoluteFilePath'; + webView.settings.setAllowFileAccess(true); return webView.loadUrl(url, {}); } 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 36862f7cbacc..b29835266717 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -132,6 +132,8 @@ abstract class WebSettingsHostApi { void setDisplayZoomControls(int instanceId, bool enabled); void setBuiltInZoomControls(int instanceId, bool enabled); + + void setAllowFileAccess(int instanceId, bool enabled); } @HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 0b78c72ecc4c..8905d7fb66e2 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/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.8.1 +version: 2.8.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 1e47c79d32b7..720fe408d96c 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -649,6 +649,7 @@ abstract class TestWebSettingsHostApi { void setUseWideViewPort(int instanceId, bool use); void setDisplayZoomControls(int instanceId, bool enabled); void setBuiltInZoomControls(int instanceId, bool enabled); + void setAllowFileAccess(int instanceId, bool enabled); static void setup(TestWebSettingsHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -940,6 +941,28 @@ abstract class TestWebSettingsHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess 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.WebSettingsHostApi.setAllowFileAccess was null, expected non-null int.'); + final bool? arg_enabled = (args[1] as bool?); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); + api.setAllowFileAccess(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } } } 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 cc29fc755067..8688a1977d83 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 @@ -430,6 +430,14 @@ void main() { true, )); }); + + test('setAllowFileAccess', () { + webSettings.setAllowFileAccess(true); + verify(mockPlatformHostApi.setAllowFileAccess( + webSettingsInstanceId, + true, + )); + }); }); group('$JavaScriptChannel', () { 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 d25d2338886b..2134de54a415 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 @@ -1,7 +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. - // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. @@ -211,6 +207,10 @@ class MockTestWebSettingsHostApi extends _i1.Mock Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), returnValueForMissingStub: null); @override + void setAllowFileAccess(int? instanceId, bool? enabled) => super.noSuchMethod( + Invocation.method(#setAllowFileAccess, [instanceId, enabled]), + returnValueForMissingStub: null); + @override String toString() => super.toString(); } 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 2b25022f9087..fed1c1113e55 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 @@ -304,6 +304,15 @@ void main() { )); }); + testWidgets('loadFile should setAllowFileAccess to true', + (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('file:///path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)); + }); + testWidgets('loadFlutterAsset', (WidgetTester tester) async { await buildWidget(tester); const String assetKey = 'test_assets/index.html'; 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 ece17ad61cb8..12e993bafa31 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 @@ -1,7 +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. - // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/webview_android_widget_test.dart. // Do not manually edit this file. @@ -120,6 +116,11 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override + _i4.Future setAllowFileAccess(bool? enabled) => + (super.noSuchMethod(Invocation.method(#setAllowFileAccess, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override String toString() => super.toString(); } From 29b637301dfbafe7f84c0c6166f2b5c4dd186697 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 19 Dec 2021 14:35:48 -0500 Subject: [PATCH 255/259] [ci] Move Android build-all CI to heavy workload (#4624) This task's memory usage is very close to the 4GB limit of the light-workload machines, and sometimes goes over and causes flaky failures. --- .cirrus.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index bcd0e49327a3..50b6cca0362a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -128,14 +128,6 @@ task: # Restore the tree to a clean state, to avoid accidental issues if # other script steps are added to this task. - git checkout . - ### Android tasks ### - - name: android-build_all_plugins - env: - BUILD_ALL_ARGS: "apk" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: web-build_all_plugins env: @@ -238,6 +230,13 @@ task: path: "**/reports/lint-results-debug.xml" type: text/xml format: android-lint + - name: android-build_all_plugins + env: + BUILD_ALL_ARGS: "apk" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: web-platform_tests env: From 6531740363e539e1751a97c56ad5c1a87c985be9 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Sun, 19 Dec 2021 14:41:00 -0500 Subject: [PATCH 256/259] [video_player] Remove test code from platform interface (#4589) This is a breaking change to video_player_platform_interface to remove Pigeon mocks from the public interface. These should never have been there, as tests in packages outside of the platform interface should be mocking the platform interface rather than the method channel, but the app-facing tests weren't rewritten when they should have been so they ended up published to support the legacy tests. This creates an unwanted non-dev dependency on `flutter_test`, in addition to promoting an anti-pattern. While making the breaking change, this also adopts `PlatformInterface`, removing `isMock` in favor of the standard mixin pattern provided by the base class. Part of https://github.com/flutter/flutter/issues/83562 --- .../CHANGELOG.md | 8 ++++ .../lib/video_player_platform_interface.dart | 44 +++++-------------- .../pubspec.yaml | 8 ++-- .../method_channel_video_player_test.dart | 3 +- .../{lib => test}/test.dart | 3 +- 5 files changed, 25 insertions(+), 41 deletions(-) rename packages/video_player/video_player_platform_interface/{lib => test}/test.dart (99%) diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index b3da9c8924ef..e0e6a11065ee 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 5.0.0 + +* **BREAKING CHANGES**: + * Updates to extending `PlatformInterface`. Removes `isMock`, in favor of the + now-standard `MockPlatformInterfaceMixin`. + * Removes test.dart from the public interface. Tests in other packages should + mock `VideoPlatformInterface` rather than the method channel. + ## 4.2.0 * Add `contentUri` to `DataSourceType`. 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 21ad972d8e06..66b6d709e9fe 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 @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart' show visibleForTesting; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel_video_player.dart'; @@ -18,37 +15,24 @@ import 'method_channel_video_player.dart'; /// (using `extends`) ensures that the subclass will get the default implementation, while /// platform implementations that `implements` this interface will be broken by newly added /// [VideoPlayerPlatform] methods. -abstract class VideoPlayerPlatform { - /// Only mock implementations should set this to true. - /// - /// Mockito mocks are implementing this class with `implements` which is forbidden for anything - /// other than mocks (see class docs). This property provides a backdoor for mockito mocks to - /// skip the verification that the class isn't implemented with `implements`. - @visibleForTesting - bool get isMock => false; +abstract class VideoPlayerPlatform extends PlatformInterface { + /// Constructs a VideoPlayerPlatform. + VideoPlayerPlatform() : super(token: _token); + + static final Object _token = Object(); static VideoPlayerPlatform _instance = MethodChannelVideoPlayer(); /// The default instance of [VideoPlayerPlatform] to use. /// - /// Platform-specific plugins should override this with their own - /// platform-specific class that extends [VideoPlayerPlatform] when they - /// register themselves. - /// /// Defaults to [MethodChannelVideoPlayer]. static VideoPlayerPlatform get instance => _instance; - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [VideoPlayerPlatform] when they + /// register themselves. static set instance(VideoPlayerPlatform instance) { - if (!instance.isMock) { - try { - instance._verifyProvidesDefaultImplementations(); - } on NoSuchMethodError catch (_) { - throw AssertionError( - 'Platform interfaces must not be implemented with `implements`'); - } - } + PlatformInterface.verifyToken(instance, _token); _instance = instance; } @@ -119,14 +103,6 @@ abstract class VideoPlayerPlatform { Future setMixWithOthers(bool mixWithOthers) { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } - - // This method makes sure that VideoPlayer isn't implemented with `implements`. - // - // See class doc for more details on why implementing this class is forbidden. - // - // This private method is called by the instance setter, which fails if the class is - // implemented with `implements`. - void _verifyProvidesDefaultImplementations() {} } /// Description of the data source used to create an instance of diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 35b30793a20f..b8404772bffa 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/video_player 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: 4.2.0 +version: 5.0.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -13,9 +13,9 @@ environment: dependencies: flutter: sdk: flutter - flutter_test: - sdk: flutter - meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: + flutter_test: + sdk: flutter pedantic: ^1.10.0 diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index f5439b844045..4d1c9b78fc34 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 @@ -8,9 +8,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; -import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'test.dart'; + class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; TextureMessage? textureMessage; diff --git a/packages/video_player/video_player_platform_interface/lib/test.dart b/packages/video_player/video_player_platform_interface/test/test.dart similarity index 99% rename from packages/video_player/video_player_platform_interface/lib/test.dart rename to packages/video_player/video_player_platform_interface/test/test.dart index b4fd81f44f41..a12ae45e59db 100644 --- a/packages/video_player/video_player_platform_interface/lib/test.dart +++ b/packages/video_player/video_player_platform_interface/test/test.dart @@ -10,8 +10,7 @@ import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'messages.dart'; +import 'package:video_player_platform_interface/messages.dart'; abstract class TestHostVideoPlayerApi { void initialize(); From 39722125e26fdbb2aff69d7b9eb260623150eeff Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Mon, 20 Dec 2021 11:30:17 -0800 Subject: [PATCH 257/259] [camera]fix a crash related to calling engine api in the background thread (#4608) --- packages/camera/camera/CHANGELOG.md | 6 +- .../ios/Runner.xcodeproj/project.pbxproj | 14 +++- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../RunnerTests/ThreadSafeEventChannelTests.m | 44 +++++++++++ .../ThreadSafeMethodChannelTests.m | 46 ++++++++++++ .../ThreadSafeTextureRegistryTests.m | 74 +++++++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 27 ++++--- .../ios/Classes/FLTThreadSafeEventChannel.h | 27 +++++++ .../ios/Classes/FLTThreadSafeEventChannel.m | 29 ++++++++ .../ios/Classes/FLTThreadSafeMethodChannel.h | 27 +++++++ .../ios/Classes/FLTThreadSafeMethodChannel.m | 29 ++++++++ .../Classes/FLTThreadSafeTextureRegistry.h | 51 +++++++++++++ .../Classes/FLTThreadSafeTextureRegistry.m | 58 +++++++++++++++ .../camera/ios/Classes/camera-umbrella.h | 3 + 14 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m create mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m create mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a6eceb957b1..f5b783d5114b 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,4 +1,8 @@ -## 0.9.4+5 +## NEXT + +* Fixed a crash in iOS when using image stream due to threading issue. + +## 0.9.4+5 * Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. * Fixes integration tests. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index feb789f2ecba..ad0ece9adbd2 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ 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 */; }; + E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; + E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; + E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ @@ -74,6 +77,9 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; + E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; + E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; @@ -107,6 +113,9 @@ 03BB766C2665316900CE5A93 /* Info.plist */, 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, + E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, + E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, + E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, @@ -239,7 +248,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 03BB76672665316900CE5A93 = { @@ -378,6 +387,9 @@ E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, + E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, + E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */, + E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1447e08231be..f4b3c1099001 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + +@interface ThreadSafeEventChannelTests : XCTestCase +@end + +@implementation ThreadSafeEventChannelTests { + FLTThreadSafeEventChannel *_channel; + XCTestExpectation *_mainThreadExpectation; +} + +- (void)setUp { + [super setUp]; + id mockEventChannel = OCMClassMock([FlutterEventChannel class]); + + _mainThreadExpectation = + [[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"]; + _channel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; + + OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self->_mainThreadExpectation fulfill]; + } + }); +} + +- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { + [_channel setStreamHandler:nil]; + [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; +} + +- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self->_channel setStreamHandler:nil]; + }); + [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m new file mode 100644 index 000000000000..a7cabbd92e37 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.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 camera; +@import XCTest; +#import + +@interface ThreadSafeMethodChannelTests : XCTestCase +@end + +@implementation ThreadSafeMethodChannelTests { + FLTThreadSafeMethodChannel *_channel; + XCTestExpectation *_mainThreadExpectation; +} + +- (void)setUp { + [super setUp]; + id mockMethodChannel = OCMClassMock([FlutterMethodChannel class]); + + _mainThreadExpectation = + [[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"]; + _channel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel]; + + OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self->_mainThreadExpectation fulfill]; + } + }); +} + +- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread { + [_channel invokeMethod:@"foo" arguments:nil]; + + [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; +} + +- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self->_channel invokeMethod:@"foo" arguments:nil]; + }); + [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m new file mode 100644 index 000000000000..73a9fb8e3c3d --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +#import + +@interface ThreadSafeTextureRegistryTests : XCTestCase +@end + +@implementation ThreadSafeTextureRegistryTests { + FLTThreadSafeTextureRegistry *_registry; + XCTestExpectation *_registerTextureExpectation; + XCTestExpectation *_unregisterTextureExpectation; + XCTestExpectation *_textureFrameAvailableExpectation; +} + +- (void)setUp { + [super setUp]; + id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); + _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry]; + + _registerTextureExpectation = [[XCTestExpectation alloc] + initWithDescription:@"registerTexture must be called in main thread"]; + _unregisterTextureExpectation = [[XCTestExpectation alloc] + initWithDescription:@"unregisterTexture must be called in main thread"]; + _textureFrameAvailableExpectation = [[XCTestExpectation alloc] + initWithDescription:@"textureFrameAvailable must be called in main thread"]; + + OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self->_registerTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self->_unregisterTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [self->_textureFrameAvailableExpectation fulfill]; + } + }); +} + +- (void)testShouldStayOnMainThreadIfCalledFromMainThread { + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [_registry registerTextureSync:anyTexture]; + [_registry textureFrameAvailable:0]; + [_registry unregisterTexture:0]; + [self waitForExpectations:@[ + _registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation + ] + timeout:1]; +} + +- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [self->_registry registerTextureSync:anyTexture]; + [self->_registry textureFrameAvailable:0]; + [self->_registry unregisterTexture:0]; + }); + [self waitForExpectations:@[ + _registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation + ] + timeout:1]; +} + +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 2c12081da807..448727963ab3 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,7 +10,10 @@ #import #import #import +#import "FLTThreadSafeEventChannel.h" #import "FLTThreadSafeFlutterResult.h" +#import "FLTThreadSafeMethodChannel.h" +#import "FLTThreadSafeTextureRegistry.h" @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @@ -305,7 +308,7 @@ @interface FLTCam : NSObject *)messen FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" binaryMessenger:messenger]; + FLTThreadSafeEventChannel *threadSafeEventChannel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; - [eventChannel setStreamHandler:_imageStreamHandler]; + [threadSafeEventChannel setStreamHandler:_imageStreamHandler]; _isStreamingImages = YES; } else { @@ -1285,10 +1290,10 @@ - (void)setUpCaptureSessionForAudio { @end @interface CameraPlugin () -@property(readonly, nonatomic) NSObject *registry; +@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; +@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -1308,7 +1313,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger { self = [super init]; NSAssert(self, @"super init cannot be nil"); - _registry = registry; + _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; [self initDeviceEventMethodChannel]; [self startOrientationListener]; @@ -1316,9 +1321,11 @@ - (instancetype)initWithRegistry:(NSObject *)registry } - (void)initDeviceEventMethodChannel { - _deviceEventMethodChannel = + FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" binaryMessenger:_messenger]; + _deviceEventMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; } - (void)startOrientationListener { @@ -1417,7 +1424,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call if (_camera) { [_camera close]; } - int64_t textureId = [self.registry registerTexture:cam]; + int64_t textureId = [self.registry registerTextureSync:cam]; _camera = cam; [result sendSuccessWithData:@{ @"cameraId" : @(textureId), @@ -1446,8 +1453,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", (unsigned long)cameraId] binaryMessenger:_messenger]; - _camera.methodChannel = methodChannel; - [methodChannel + FLTThreadSafeMethodChannel *threadSafeMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; + _camera.methodChannel = threadSafeMethodChannel; + [threadSafeMethodChannel invokeMethod:@"initialized" arguments:@{ @"previewWidth" : @(_camera.previewSize.width), diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h new file mode 100644 index 000000000000..dd84a6733d64 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper for FlutterEventChannel that always sends events on the main thread + */ +@interface FLTThreadSafeEventChannel : NSObject + +/** + * Creates a FLTThreadSafeEventChannel by wrapping a FlutterEventChannel object. + * @param channel The FlutterEventChannel object to be wrapped. + */ +- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel; + +/* + * Registers a handler for stream setup requests from the Flutter side on main thread. + */ +- (void)setStreamHandler:(nullable NSObject *)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m new file mode 100644 index 000000000000..418d83c37849 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeEventChannel.h" + +@implementation FLTThreadSafeEventChannel { + FlutterEventChannel *_channel; +} + +- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel { + self = [super init]; + if (self) { + _channel = channel; + } + return self; +} + +- (void)setStreamHandler:(NSObject *)handler { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_channel setStreamHandler:handler]; + }); + } else { + [_channel setStreamHandler:handler]; + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h new file mode 100644 index 000000000000..db99d0f7f66a --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper for FlutterMethodChannel that always invokes messages on the main thread + */ +@interface FLTThreadSafeMethodChannel : NSObject + +/** + * Creates a FLTThreadSafeMethodChannel by wrapping a FlutterMethodChannel object. + * @param channel The FlutterMethodChannel object to be wrapped. + */ +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel; + +/** + * Invokes the specified flutter method with the specified arguments on main thread. + */ +- (void)invokeMethod:(NSString *)method arguments:(nullable id)arguments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m new file mode 100644 index 000000000000..e62fd5bb3af8 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeMethodChannel.h" + +@implementation FLTThreadSafeMethodChannel { + FlutterMethodChannel *_channel; +} + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _channel = channel; + } + return self; +} + +- (void)invokeMethod:(NSString *)method arguments:(id)arguments { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_channel invokeMethod:method arguments:arguments]; + }); + } else { + [_channel invokeMethod:method arguments:arguments]; + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h new file mode 100644 index 000000000000..34ee4400bcc1 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper for FlutterTextureRegistry that always sends events on the main thread + */ +@interface FLTThreadSafeTextureRegistry : NSObject + +/** + * Creates a FLTThreadSafeTextureRegistry by wrapping an object conforming to + * FlutterTextureRegistry. + * @param registry The FlutterTextureRegistry object to be wrapped. + */ +- (instancetype)initWithTextureRegistry:(NSObject *)registry; + +/** + * Registers a `FlutterTexture` for usage in Flutter and returns an id that can be used to reference + * that texture when calling into Flutter with channels. Textures must be registered on the + * main thread. On success returns the pointer to the registered texture, else returns 0. + * + * Runs on main thread. + */ +- (int64_t)registerTextureSync:(NSObject *)texture; + +/** + * Notifies Flutter that the content of the previously registered texture has been updated. + * + * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. + * + * Runs on main thread. + */ +- (void)textureFrameAvailable:(int64_t)textureId; + +/** + * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures + * must be unregistered on the main thread. + * + * Runs on main thread. + * + * @param textureId The result that was previously returned from `registerTexture:`. + */ +- (void)unregisterTexture:(int64_t)textureId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m new file mode 100644 index 000000000000..103c61cce6fd --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeTextureRegistry.h" + +@implementation FLTThreadSafeTextureRegistry { + NSObject *_registry; +} + +- (instancetype)initWithTextureRegistry:(NSObject *)registry { + self = [super init]; + if (self) { + _registry = registry; + } + return self; +} + +- (int64_t)registerTextureSync:(NSObject *)texture { + if (!NSThread.isMainThread) { + __block int64_t textureId; + // We cannot use async API (with completion block) because completion block does not work for + // separate functions (e.g. `dispose` and `create` are separately registered functions). It's + // hard to tell if the developers had made implicit assumption of the synchronous nature of the + // original API when implementing this plugin. Use dispatch_sync to keep + // FlutterTextureRegistry's sychronous API, so that we don't introduce new potential race + // conditions. We do not break priority inversion here since it's the background thread waiting + // for main thread. + dispatch_sync(dispatch_get_main_queue(), ^{ + textureId = [self->_registry registerTexture:texture]; + }); + return textureId; + } else { + return [_registry registerTexture:texture]; + } +} + +- (void)textureFrameAvailable:(int64_t)textureId { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_registry textureFrameAvailable:textureId]; + }); + } else { + [_registry textureFrameAvailable:textureId]; + } +} + +- (void)unregisterTexture:(int64_t)textureId { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_registry unregisterTexture:textureId]; + }); + } else { + [_registry unregisterTexture:textureId]; + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index b0fd493b24df..c55276e31147 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,7 +4,10 @@ #import #import +#import #import +#import +#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[]; From c9c234a53e563a0312f96ca7a77a8d015bfaab5f Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 20 Dec 2021 14:41:55 -0500 Subject: [PATCH 258/259] [video_player] Add 5.0 interface support (#4627) Allows the other video_player packages to use either 4.x or 5.x of `video_player_platform_interface`, since the only breaking change doesn't affect them (as it just removes test code that they no longer use). This allows clients of `video_player` to no longer have a transitive dependency on test packages, but doesn't create any version lock with any unendorsed implementations that might exist. Fixes https://github.com/flutter/flutter/issues/83562 --- .../video_player/video_player/CHANGELOG.md | 5 ++ .../video_player/video_player/CONTRIBUTING.md | 49 ------------------- .../video_player/video_player/pubspec.yaml | 4 +- .../video_player_web/CHANGELOG.md | 7 ++- .../video_player_web/pubspec.yaml | 4 +- 5 files changed, 15 insertions(+), 54 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 6f2a2b7250d0..b87ac4bbeded 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.9 + +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + ## 2.2.8 * Changes the way the `VideoPlayerPlatform` instance is cached in the diff --git a/packages/video_player/video_player/CONTRIBUTING.md b/packages/video_player/video_player/CONTRIBUTING.md index 15c48038f6fc..387551bda2f6 100644 --- a/packages/video_player/video_player/CONTRIBUTING.md +++ b/packages/video_player/video_player/CONTRIBUTING.md @@ -31,52 +31,3 @@ pigeon, not your version or the version on master. In either case, the configuration will be obtained automatically from the `pigeons/messages.dart` file (see `configurePigeon` at the bottom of that file). - -While contributing, you may also want to set the following dependency -overrides: - -```yaml -dependency_overrides: - video_player_platform_interface: - path: - ../video_player_platform_interface - video_player_web: - path: - ../video_player_web -``` - -## Publishing plugin updates that span multiple plugin packages - -If your change affects both the interface package and the -implementation packages, then you will need to publish a version of -the plugin in between landing the interface changes and the -implementation changes, since the implementations depend on the -interface via pub. - -To do this, follow these steps: - -1. Create a PR that has all the changes, and update the -`pubspec.yaml`s to have path-based dependency overrides as described -in the "Updating pigeon-generated files" section above. - -2. Upload that PR and get it reviewed and into a state where the only -test failure is the one complaining that you can't publish a package -that has dependency overrides. - -3. Create a PR that's a subset of the one in the previous step that -only includes the interface changes, with no dependency overrides, and -submit that. - -4. Once you have had that reviewed and landed, publish the interface -parts of the plugin to pub. - -5. Now, update the original full PR to not use dependency overrides -but to instead refer to the new version of the plugin, and sync it to -master (so that the interface changes are gone from the PR). Submit -that PR. - -6. Once you have had _that_ PR reviewed and landed, publish the -implementation parts of the plugin to pub. - -You may need to publish each implementation package independently of -the main package also, depending on exactly what your change entails. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index f8c091786526..3ae9c0dfd301 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/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.8 +version: 2.2.9 environment: sdk: ">=2.14.0 <3.0.0" @@ -24,7 +24,7 @@ dependencies: flutter: sdk: flutter meta: ^1.3.0 - video_player_platform_interface: ^4.2.0 + video_player_platform_interface: ">=4.2.0 <6.0.0" video_player_web: ^2.0.0 html: ^0.15.0 diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 4eb7c9d610b5..13cbf2e24dae 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,7 +1,12 @@ +## 2.0.5 + +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + ## 2.0.4 * Adopt `video_player_platform_interface` 4.2 and opt out of `contentUri` data source. - + ## 2.0.3 * Add `implements` to pubspec. diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index ac0754b1a5d0..ec2377ea1fb1 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/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.4 +version: 2.0.5 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,7 +23,7 @@ dependencies: sdk: flutter meta: ^1.3.0 pedantic: ^1.10.0 - video_player_platform_interface: ^4.2.0 + video_player_platform_interface: ">=4.2.0 <6.0.0" dev_dependencies: flutter_test: From 19468e0950f5ec237ca9319c20055ff08ec963c3 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 20 Dec 2021 14:44:17 -0800 Subject: [PATCH 259/259] Revert "[camera]Fix crash due to calling engine APIs from background thread" (#4630) --- packages/camera/camera/CHANGELOG.md | 6 +- .../ios/Runner.xcodeproj/project.pbxproj | 14 +--- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../RunnerTests/ThreadSafeEventChannelTests.m | 44 ----------- .../ThreadSafeMethodChannelTests.m | 46 ------------ .../ThreadSafeTextureRegistryTests.m | 74 ------------------- .../camera/camera/ios/Classes/CameraPlugin.m | 27 +++---- .../ios/Classes/FLTThreadSafeEventChannel.h | 27 ------- .../ios/Classes/FLTThreadSafeEventChannel.m | 29 -------- .../ios/Classes/FLTThreadSafeMethodChannel.h | 27 ------- .../ios/Classes/FLTThreadSafeMethodChannel.m | 29 -------- .../Classes/FLTThreadSafeTextureRegistry.h | 51 ------------- .../Classes/FLTThreadSafeTextureRegistry.m | 58 --------------- .../camera/ios/Classes/camera-umbrella.h | 3 - 14 files changed, 12 insertions(+), 425 deletions(-) delete mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m delete mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m delete mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h delete mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index f5b783d5114b..1a6eceb957b1 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,8 +1,4 @@ -## NEXT - -* Fixed a crash in iOS when using image stream due to threading issue. - -## 0.9.4+5 +## 0.9.4+5 * Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. * Fixes integration tests. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index ad0ece9adbd2..feb789f2ecba 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -20,9 +20,6 @@ 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 */; }; - E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; - E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; - E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ @@ -77,9 +74,6 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; - E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; - E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; @@ -113,9 +107,6 @@ 03BB766C2665316900CE5A93 /* Info.plist */, 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, - E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, - E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, - E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, @@ -248,7 +239,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 03BB76672665316900CE5A93 = { @@ -387,9 +378,6 @@ E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, - E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, - E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */, - E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f4b3c1099001..1447e08231be 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - -@interface ThreadSafeEventChannelTests : XCTestCase -@end - -@implementation ThreadSafeEventChannelTests { - FLTThreadSafeEventChannel *_channel; - XCTestExpectation *_mainThreadExpectation; -} - -- (void)setUp { - [super setUp]; - id mockEventChannel = OCMClassMock([FlutterEventChannel class]); - - _mainThreadExpectation = - [[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"]; - _channel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; - - OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { - if (NSThread.isMainThread) { - [self->_mainThreadExpectation fulfill]; - } - }); -} - -- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { - [_channel setStreamHandler:nil]; - [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; -} - -- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self->_channel setStreamHandler:nil]; - }); - [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; -} - -@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m deleted file mode 100644 index a7cabbd92e37..000000000000 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import camera; -@import XCTest; -#import - -@interface ThreadSafeMethodChannelTests : XCTestCase -@end - -@implementation ThreadSafeMethodChannelTests { - FLTThreadSafeMethodChannel *_channel; - XCTestExpectation *_mainThreadExpectation; -} - -- (void)setUp { - [super setUp]; - id mockMethodChannel = OCMClassMock([FlutterMethodChannel class]); - - _mainThreadExpectation = - [[XCTestExpectation alloc] initWithDescription:@"invokeMethod must be called in main thread"]; - _channel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel]; - - OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]]) - .andDo(^(NSInvocation *invocation) { - if (NSThread.isMainThread) { - [self->_mainThreadExpectation fulfill]; - } - }); -} - -- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread { - [_channel invokeMethod:@"foo" arguments:nil]; - - [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; -} - -- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self->_channel invokeMethod:@"foo" arguments:nil]; - }); - [self waitForExpectations:@[ _mainThreadExpectation ] timeout:1]; -} - -@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m deleted file mode 100644 index 73a9fb8e3c3d..000000000000 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import camera; -@import XCTest; -#import - -@interface ThreadSafeTextureRegistryTests : XCTestCase -@end - -@implementation ThreadSafeTextureRegistryTests { - FLTThreadSafeTextureRegistry *_registry; - XCTestExpectation *_registerTextureExpectation; - XCTestExpectation *_unregisterTextureExpectation; - XCTestExpectation *_textureFrameAvailableExpectation; -} - -- (void)setUp { - [super setUp]; - id mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); - _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry]; - - _registerTextureExpectation = [[XCTestExpectation alloc] - initWithDescription:@"registerTexture must be called in main thread"]; - _unregisterTextureExpectation = [[XCTestExpectation alloc] - initWithDescription:@"unregisterTexture must be called in main thread"]; - _textureFrameAvailableExpectation = [[XCTestExpectation alloc] - initWithDescription:@"textureFrameAvailable must be called in main thread"]; - - OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) { - if (NSThread.isMainThread) { - [self->_registerTextureExpectation fulfill]; - } - }); - - OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) { - if (NSThread.isMainThread) { - [self->_unregisterTextureExpectation fulfill]; - } - }); - - OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) { - if (NSThread.isMainThread) { - [self->_textureFrameAvailableExpectation fulfill]; - } - }); -} - -- (void)testShouldStayOnMainThreadIfCalledFromMainThread { - NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); - [_registry registerTextureSync:anyTexture]; - [_registry textureFrameAvailable:0]; - [_registry unregisterTexture:0]; - [self waitForExpectations:@[ - _registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation - ] - timeout:1]; -} - -- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); - [self->_registry registerTextureSync:anyTexture]; - [self->_registry textureFrameAvailable:0]; - [self->_registry unregisterTexture:0]; - }); - [self waitForExpectations:@[ - _registerTextureExpectation, _unregisterTextureExpectation, _textureFrameAvailableExpectation - ] - timeout:1]; -} - -@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 448727963ab3..2c12081da807 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,10 +10,7 @@ #import #import #import -#import "FLTThreadSafeEventChannel.h" #import "FLTThreadSafeFlutterResult.h" -#import "FLTThreadSafeMethodChannel.h" -#import "FLTThreadSafeTextureRegistry.h" @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @@ -308,7 +305,7 @@ @interface FLTCam : NSObject *)messen FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" binaryMessenger:messenger]; - FLTThreadSafeEventChannel *threadSafeEventChannel = - [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; - [threadSafeEventChannel setStreamHandler:_imageStreamHandler]; + [eventChannel setStreamHandler:_imageStreamHandler]; _isStreamingImages = YES; } else { @@ -1290,10 +1285,10 @@ - (void)setUpCaptureSessionForAudio { @end @interface CameraPlugin () -@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; +@property(readonly, nonatomic) NSObject *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel; +@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -1313,7 +1308,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger { self = [super init]; NSAssert(self, @"super init cannot be nil"); - _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; + _registry = registry; _messenger = messenger; [self initDeviceEventMethodChannel]; [self startOrientationListener]; @@ -1321,11 +1316,9 @@ - (instancetype)initWithRegistry:(NSObject *)registry } - (void)initDeviceEventMethodChannel { - FlutterMethodChannel *methodChannel = + _deviceEventMethodChannel = [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" binaryMessenger:_messenger]; - _deviceEventMethodChannel = - [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; } - (void)startOrientationListener { @@ -1424,7 +1417,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call if (_camera) { [_camera close]; } - int64_t textureId = [self.registry registerTextureSync:cam]; + int64_t textureId = [self.registry registerTexture:cam]; _camera = cam; [result sendSuccessWithData:@{ @"cameraId" : @(textureId), @@ -1453,10 +1446,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", (unsigned long)cameraId] binaryMessenger:_messenger]; - FLTThreadSafeMethodChannel *threadSafeMethodChannel = - [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; - _camera.methodChannel = threadSafeMethodChannel; - [threadSafeMethodChannel + _camera.methodChannel = methodChannel; + [methodChannel invokeMethod:@"initialized" arguments:@{ @"previewWidth" : @(_camera.previewSize.width), diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h deleted file mode 100644 index dd84a6733d64..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * Wrapper for FlutterEventChannel that always sends events on the main thread - */ -@interface FLTThreadSafeEventChannel : NSObject - -/** - * Creates a FLTThreadSafeEventChannel by wrapping a FlutterEventChannel object. - * @param channel The FlutterEventChannel object to be wrapped. - */ -- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel; - -/* - * Registers a handler for stream setup requests from the Flutter side on main thread. - */ -- (void)setStreamHandler:(nullable NSObject *)handler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m deleted file mode 100644 index 418d83c37849..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTThreadSafeEventChannel.h" - -@implementation FLTThreadSafeEventChannel { - FlutterEventChannel *_channel; -} - -- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel { - self = [super init]; - if (self) { - _channel = channel; - } - return self; -} - -- (void)setStreamHandler:(NSObject *)handler { - if (!NSThread.isMainThread) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_channel setStreamHandler:handler]; - }); - } else { - [_channel setStreamHandler:handler]; - } -} - -@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h deleted file mode 100644 index db99d0f7f66a..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * Wrapper for FlutterMethodChannel that always invokes messages on the main thread - */ -@interface FLTThreadSafeMethodChannel : NSObject - -/** - * Creates a FLTThreadSafeMethodChannel by wrapping a FlutterMethodChannel object. - * @param channel The FlutterMethodChannel object to be wrapped. - */ -- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel; - -/** - * Invokes the specified flutter method with the specified arguments on main thread. - */ -- (void)invokeMethod:(NSString *)method arguments:(nullable id)arguments; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m deleted file mode 100644 index e62fd5bb3af8..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTThreadSafeMethodChannel.h" - -@implementation FLTThreadSafeMethodChannel { - FlutterMethodChannel *_channel; -} - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _channel = channel; - } - return self; -} - -- (void)invokeMethod:(NSString *)method arguments:(id)arguments { - if (!NSThread.isMainThread) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_channel invokeMethod:method arguments:arguments]; - }); - } else { - [_channel invokeMethod:method arguments:arguments]; - } -} - -@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h deleted file mode 100644 index 34ee4400bcc1..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * Wrapper for FlutterTextureRegistry that always sends events on the main thread - */ -@interface FLTThreadSafeTextureRegistry : NSObject - -/** - * Creates a FLTThreadSafeTextureRegistry by wrapping an object conforming to - * FlutterTextureRegistry. - * @param registry The FlutterTextureRegistry object to be wrapped. - */ -- (instancetype)initWithTextureRegistry:(NSObject *)registry; - -/** - * Registers a `FlutterTexture` for usage in Flutter and returns an id that can be used to reference - * that texture when calling into Flutter with channels. Textures must be registered on the - * main thread. On success returns the pointer to the registered texture, else returns 0. - * - * Runs on main thread. - */ -- (int64_t)registerTextureSync:(NSObject *)texture; - -/** - * Notifies Flutter that the content of the previously registered texture has been updated. - * - * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. - * - * Runs on main thread. - */ -- (void)textureFrameAvailable:(int64_t)textureId; - -/** - * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures - * must be unregistered on the main thread. - * - * Runs on main thread. - * - * @param textureId The result that was previously returned from `registerTexture:`. - */ -- (void)unregisterTexture:(int64_t)textureId; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m deleted file mode 100644 index 103c61cce6fd..000000000000 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTThreadSafeTextureRegistry.h" - -@implementation FLTThreadSafeTextureRegistry { - NSObject *_registry; -} - -- (instancetype)initWithTextureRegistry:(NSObject *)registry { - self = [super init]; - if (self) { - _registry = registry; - } - return self; -} - -- (int64_t)registerTextureSync:(NSObject *)texture { - if (!NSThread.isMainThread) { - __block int64_t textureId; - // We cannot use async API (with completion block) because completion block does not work for - // separate functions (e.g. `dispose` and `create` are separately registered functions). It's - // hard to tell if the developers had made implicit assumption of the synchronous nature of the - // original API when implementing this plugin. Use dispatch_sync to keep - // FlutterTextureRegistry's sychronous API, so that we don't introduce new potential race - // conditions. We do not break priority inversion here since it's the background thread waiting - // for main thread. - dispatch_sync(dispatch_get_main_queue(), ^{ - textureId = [self->_registry registerTexture:texture]; - }); - return textureId; - } else { - return [_registry registerTexture:texture]; - } -} - -- (void)textureFrameAvailable:(int64_t)textureId { - if (!NSThread.isMainThread) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_registry textureFrameAvailable:textureId]; - }); - } else { - [_registry textureFrameAvailable:textureId]; - } -} - -- (void)unregisterTexture:(int64_t)textureId { - if (!NSThread.isMainThread) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_registry unregisterTexture:textureId]; - }); - } else { - [_registry unregisterTexture:textureId]; - } -} - -@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index c55276e31147..b0fd493b24df 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,10 +4,7 @@ #import #import -#import #import -#import -#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[];