From 53eed81e464510e8530d56327f366772821abf9e Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 25 Oct 2021 15:07:27 +0200 Subject: [PATCH 01/10] Add platform interface method `loadRequest`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yusuf Dağ --- .../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 +- .../test/src/types/webview_request_test.dart | 31 ++++++++++ 7 files changed, 122 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/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..00cbca8fcd59 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; + +void main() { + 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 d7ffb451dccd7103cbb1567eea271374d03a5d1b Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 25 Oct 2021 15:17:34 +0200 Subject: [PATCH 02/10] Add test --- .../test/src/types/webview_request_test.dart | 5 +++++ 1 file changed, 5 insertions(+) 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 index 00cbca8fcd59..e46c75fc7edd 100644 --- 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 @@ -4,6 +4,11 @@ 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; From 6cf641125463726b4d6c2f1906b765c5ebc6f5ce Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 25 Oct 2021 15:42:41 +0200 Subject: [PATCH 03/10] Add missing license --- .../test/src/types/webview_request_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index e46c75fc7edd..5d2b568fe5dd 100644 --- 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 @@ -1,5 +1,8 @@ -import 'dart:typed_data'; +// Copyright 2013 The Flutter Authors. All 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'; From 3bd7d4b165eaaebf9675ab61f4196b91c7bb1731 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Mon, 25 Oct 2021 15:57:04 +0200 Subject: [PATCH 04/10] Add test --- .../webview_method_channel_test.dart | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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..e21f9c191c69 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 @@ -101,6 +101,30 @@ 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('currentUrl', () async { final String? currentUrl = await webViewPlatform.currentUrl(); From b9274149a3708ad7b9c87755b342bc93b2105680 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 1 Nov 2021 20:49:39 +0100 Subject: [PATCH 05/10] Add missing test --- .../webview_method_channel_test.dart | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 e21f9c191c69..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'; @@ -125,6 +127,32 @@ void main() { ); }); + 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(); From 3f27a3a058c23f87a0cda675ad945b6ca57c90d3 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 5 Nov 2021 16:18:32 +0100 Subject: [PATCH 06/10] WIP Native Android & iOS implementations for loadRequest method. Co-authored-by: Yusuf --- .../webviewflutter/FlutterWebView.java | 91 ++++++ .../webviewflutter/HttpRequestManager.java | 203 ++++++++++++ .../webviewflutter/WebViewRequest.java | 107 +++++++ .../HttpRequestManagerTest.java | 296 ++++++++++++++++++ .../webviewflutter/WebViewRequestTest.java | 74 +++++ .../example/lib/main.dart | 20 ++ .../example/lib/web_view.dart | 5 + .../webview_flutter_android/pubspec.yaml | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 10 +- .../example/ios/RunnerTests/FLTWebViewTests.m | 192 ++++++++++++ .../example/lib/main.dart | 20 ++ .../example/lib/web_view.dart | 5 + .../ios/Classes/FlutterWebView.h | 7 + .../ios/Classes/FlutterWebView.m | 90 ++++-- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 15 files changed, 1095 insertions(+), 29 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewRequest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/HttpRequestManagerTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewRequestTest.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 ed14107220b8..6177d8d26b75 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 @@ -9,6 +9,7 @@ import android.hardware.display.DisplayManager; import android.os.Build; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.view.View; import android.webkit.DownloadListener; @@ -20,6 +21,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.os.HandlerCompat; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -28,6 +30,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; public class FlutterWebView implements PlatformView, MethodCallHandler { @@ -36,6 +40,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private final MethodChannel methodChannel; private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; + private final HttpRequestManager httpRequestManager; // Verifies that a url opened by `Window.open` has a secure url. private class FlutterWebChromeClient extends WebChromeClient { @@ -96,6 +101,10 @@ public void onProgressChanged(WebView view, int progress) { (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); + httpRequestManager = + HttpRequestManagerFactory.create( + Executors.newCachedThreadPool(), HandlerCompat.createAsync(Looper.getMainLooper())); + this.methodChannel = methodChannel; this.methodChannel.setMethodCallHandler(this); @@ -223,6 +232,9 @@ public void onMethodCall(MethodCall methodCall, Result result) { case "loadUrl": loadUrl(methodCall, result); break; + case "loadRequest": + loadRequest(methodCall, result); + break; case "updateSettings": updateSettings(methodCall, result); break; @@ -292,6 +304,67 @@ private void loadUrl(MethodCall methodCall, Result result) { result.success(null); } + private void loadRequest(MethodCall methodCall, Result result) { + final WebViewRequest webViewRequest = buildWebViewRequest(methodCall); + if (webViewRequest == null) { + result.error("missing_args", "Missing arguments", null); + return; + } + switch (webViewRequest.getMethod()) { + case GET: + webView.loadUrl(webViewRequest.getUri(), webViewRequest.getHeaders()); + result.success(null); + break; + case POST: + if (webViewRequest.getHeaders().isEmpty()) { + webView.postUrl(webViewRequest.getUri(), webViewRequest.getBody()); + result.success(null); + } else { + // Execute the request manually to be able to provide headers with the post request. + httpRequestManager.requestAsync( + webViewRequest, + new HttpRequestCallback() { + @Override + public void onComplete(String content) { + if (!webView.isAttachedToWindow()) { + result.error( + "webview_destroyed", + "Could not complete the post request because the webview is destroyed", + null); + } else { + // TODO (BeMacized): Check if this still works in the case of a server side redirect + webView.loadDataWithBaseURL( + webViewRequest.getUri(), content, "text/html", "UTF-8", null); + result.success(null); + } + } + + @Override + public void onError(Exception error) { + result.error("request_failed", "HttpURLConnection has failed", null); + } + }); + } + break; + default: + result.error("unsupported_method", "Unsupported HTTP method", null); + } + } + + private WebViewRequest buildWebViewRequest(MethodCall methodCall) { + Map request = (Map) methodCall.arguments; + if (request == null) { + return null; + } + + Map requestObject = (Map) request.get("request"); + if (requestObject == null) { + return null; + } + + return WebViewRequest.fromMap(requestObject); + } + private void canGoBack(Result result) { result.success(webView.canGoBack()); } @@ -492,4 +565,22 @@ public void dispose() { } webView.destroy(); } + + /** Factory class for creating a {@link HttpRequestManager} */ + static class HttpRequestManagerFactory { + /** + * Creates a {@link HttpRequestManager}. + * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param executor a {@link Executor} to run network request on background thread. + * @param resultHandler a {@link Handler} to communicate back with main thread. + * @return The new {@link HttpRequestManager} object. + */ + @VisibleForTesting + public static HttpRequestManager create(Executor executor, Handler resultHandler) { + return new HttpRequestManager(executor, resultHandler); + } + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java new file mode 100644 index 000000000000..da715a802b4d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java @@ -0,0 +1,203 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import androidx.annotation.VisibleForTesting; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.Executor; + +/** Defines callback methods for the HttpRequestManager. */ +interface HttpRequestCallback { + void onComplete(String result); + + void onError(Exception error); +} + +/** + * Works around on Android WebView postUrl method to accept headers. + * + *

Android WebView does not provide a post request method that accepts headers. Only method that + * is provided is {@link android.webkit.WebView#postUrl(String, byte[])} and it accepts only URL and + * HTTP body. CustomHttpPostRequest is implemented to provide this feature since adding a header to + * post requests is a feature that is likely to be wanted. + * + *

In the implementation, {@link HttpURLConnection} is used to create a post request with the + * HTTP headers and the HTTP body. + */ +public class HttpRequestManager { + private final Executor executor; + private final Handler resultHandler; + + HttpRequestManager(Executor executor, Handler resultHandler) { + this.executor = executor; + this.resultHandler = resultHandler; + } + + /** + * Executes the given HTTP request in a background thread. See https://developer.android.com/guide/background/threading. + * + * @param request {@link WebViewRequest} to execute. + * @param callback methods to invoke after the HTTP request has completed. + */ + public void requestAsync(final WebViewRequest request, final HttpRequestCallback callback) { + executor.execute( + new Runnable() { + @Override + public void run() { + try { + String responseResult = request(request); + notifyComplete(responseResult, callback); + } catch (IOException e) { + notifyError(e, callback); + } + } + }); + } + + /** + * Executes the given HTTP request synchronously. + * + * @param request {@link WebViewRequest} to execute. + * @return The response body as a String. + */ + public String request(WebViewRequest request) throws IOException { + URL url = URLFactory.create(request.getUri()); + HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); + try { + // Basic request configuration + httpURLConnection.setConnectTimeout(5000); + httpURLConnection.setRequestMethod(request.getMethod().getValue().toUpperCase()); + + // Set HTTP headers + for (Map.Entry entry : request.getHeaders().entrySet()) { + httpURLConnection.setRequestProperty(entry.getKey(), entry.getValue()); + } + + // Set HTTP body + if (request.getBody() != null && request.getBody().length > 0) { + // Used to enable streaming of a HTTP request body without internal buffering, + // when the content length is known in advance. It improves the performance + // because otherwise HTTPUrlConnection will be forced to buffer the complete + // request body in memory before it is transmitted, wasting (and possibly exhausting) + // heap and increasing latency. + // httpURLConnection.setFixedLengthStreamingMode(request.getBody().length); + + httpURLConnection.setDoOutput(true); + OutputStream os = BufferedOutputStreamFactory.create(httpURLConnection.getOutputStream()); + os.write(request.getBody()); + os.flush(); + os.close(); + } + + // Collect and return response body + String line = ""; + StringBuilder contentBuilder = new StringBuilder(); + BufferedReader rd = + BufferedReaderFactory.create( + InputStreamReaderFactory.create(httpURLConnection.getInputStream())); + while ((line = rd.readLine()) != null) { + contentBuilder.append(line); + } + return contentBuilder.toString(); + } finally { + httpURLConnection.disconnect(); + } + } + + private void notifyComplete(final String responseResult, final HttpRequestCallback callback) { + resultHandler.post( + new Runnable() { + @Override + public void run() { + callback.onComplete(responseResult); + } + }); + } + + private void notifyError(final Exception error, final HttpRequestCallback callback) { + resultHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(error); + } + }); + } + /** Factory class for creating a {@link URL} */ + static class URLFactory { + /** + * Creates a {@link URL}. + * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param url to create the instance for. + * @return The new {@link URL} object. + */ + @VisibleForTesting + public static URL create(String url) throws MalformedURLException { + return new URL(url); + } + } + /** Factory class for creating a {@link BufferedOutputStream} */ + static class BufferedOutputStreamFactory { + /** + * Creates a {@link BufferedOutputStream}. + * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param stream to create the instance for. + * @return The new {@link BufferedOutputStream} object. + */ + @VisibleForTesting + public static BufferedOutputStream create(OutputStream stream) { + return new BufferedOutputStream(stream); + } + } + /** Factory class for creating a {@link BufferedReader} */ + static class BufferedReaderFactory { + /** + * Creates a {@link BufferedReader}. + * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param stream to create the instance for. + * @return The new {@link BufferedReader} object. + */ + @VisibleForTesting + public static BufferedReader create(InputStreamReader stream) { + return new BufferedReader(stream); + } + } + /** Factory class for creating a {@link InputStreamReader} */ + static class InputStreamReaderFactory { + /** + * Creates a {@link InputStreamReader}. + * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param stream to create the instance for. + * @return The new {@link InputStreamReader} object. + */ + @VisibleForTesting + public static InputStreamReader create(InputStream stream) { + return new InputStreamReader(stream); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewRequest.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewRequest.java new file mode 100644 index 000000000000..f5e80c3a178e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewRequest.java @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import java.util.Collections; +import java.util.Map; + +/** + * Defines the supported HTTP methods for loading a page in the {@link android.webkit.WebView} and + * the {@link HttpRequestManager}. + */ +enum WebViewLoadMethod { + GET("get"), + + POST("post"); + + private final String value; + + WebViewLoadMethod(String value) { + this.value = value; + } + + /** Converts to WebViewLoadMethod to String format. */ + public String serialize() { + return getValue(); + } + + /** Returns the enum value. */ + public String getValue() { + return value; + } + + /** Converts to String to WebViewLoadMethod format. */ + public static WebViewLoadMethod deserialize(String value) { + for (WebViewLoadMethod webViewLoadMethod : WebViewLoadMethod.values()) { + if (webViewLoadMethod.value.equals(value)) { + return webViewLoadMethod; + } + } + throw new IllegalArgumentException("No enum value found for '" + value + "'."); + } +} + +/** + * Creates a HTTP request object. + * + *

Defines the parameters that can be used to load a page in the {@link android.webkit.WebView} + * and the {@link HttpRequestManager}. + */ +public class WebViewRequest { + private final String uri; + private final WebViewLoadMethod method; + private final Map headers; + private final byte[] body; + + WebViewRequest(String uri, WebViewLoadMethod method, Map headers, byte[] body) { + this.uri = uri; + this.method = method; + this.headers = headers == null ? Collections.emptyMap() : headers; + this.body = body; + } + + /** + * Deserializes the request and the url to WebViewRequest instance. + * + * @param requestObject is the {@link io.flutter.plugin.common.MethodCall#arguments} to build + * WebViewRequest instance. + */ + @SuppressWarnings("unchecked") + static WebViewRequest fromMap(Map requestObject) { + String uri = (String) requestObject.get("uri"); + if (uri == null) { + return null; + } + + Map headers = (Map) requestObject.get("headers"); + + WebViewLoadMethod invokedMethod = + WebViewLoadMethod.deserialize((String) requestObject.get("method")); + + byte[] httpBody = (byte[]) requestObject.get("body"); + + return new WebViewRequest(uri, invokedMethod, headers, httpBody); + } + + /** Returns HTTP method in WebViewLoadMethod format. */ + public WebViewLoadMethod getMethod() { + return method; + } + + /** Returns base url. */ + public String getUri() { + return uri; + } + + /** Returns HTTP headers. */ + public Map getHeaders() { + return headers; + } + + /** Returns HTTP body. */ + public byte[] getBody() { + return body; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/HttpRequestManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/HttpRequestManagerTest.java new file mode 100644 index 000000000000..b2653097d471 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/HttpRequestManagerTest.java @@ -0,0 +1,296 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +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.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Handler; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class HttpRequestManagerTest { + + Executor mockExecutor; + Handler mockHandler; + HttpRequestManager httpRequestManager; + MockedStatic mockedURLFactory; + URL mockUrl; + MockedStatic mockedBufferedOutputStreamFactory; + BufferedOutputStream mockBufferedOutputStream; + MockedStatic mockedBufferedReaderFactory; + BufferedReader mockBufferedReader = mock(BufferedReader.class); + MockedStatic mockedInputStreamReaderFactory; + InputStreamReader mockInputStreamReader = mock(InputStreamReader.class); + + @Before + public void setup() { + mockExecutor = mock(Executor.class); + mockHandler = mock(Handler.class); + httpRequestManager = spy(new HttpRequestManager(mockExecutor, mockHandler)); + + mockUrl = mock(URL.class); + mockedURLFactory = mockStatic(HttpRequestManager.URLFactory.class); + mockedURLFactory + .when(() -> HttpRequestManager.URLFactory.create(ArgumentMatchers.any())) + .thenReturn(mockUrl); + + mockBufferedOutputStream = mock(BufferedOutputStream.class); + mockedBufferedOutputStreamFactory = + mockStatic(HttpRequestManager.BufferedOutputStreamFactory.class); + mockedBufferedOutputStreamFactory + .when( + () -> + HttpRequestManager.BufferedOutputStreamFactory.create( + ArgumentMatchers.any())) + .thenReturn(mockBufferedOutputStream); + + mockBufferedReader = mock(BufferedReader.class); + mockedBufferedReaderFactory = mockStatic(HttpRequestManager.BufferedReaderFactory.class); + mockedBufferedReaderFactory + .when( + () -> + HttpRequestManager.BufferedReaderFactory.create( + ArgumentMatchers.any())) + .thenReturn(mockBufferedReader); + + mockInputStreamReader = mock(InputStreamReader.class); + mockedInputStreamReaderFactory = mockStatic(HttpRequestManager.InputStreamReaderFactory.class); + mockedInputStreamReaderFactory + .when( + () -> + HttpRequestManager.InputStreamReaderFactory.create( + ArgumentMatchers.any())) + .thenReturn(mockInputStreamReader); + } + + @After + public void tearDown() { + mockedURLFactory.close(); + mockedBufferedOutputStreamFactory.close(); + mockedBufferedReaderFactory.close(); + mockedInputStreamReaderFactory.close(); + } + + @Test + public void request_shouldBuildAndExecuteRequest() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + Map headers = + new HashMap() { + { + put("3", "3"); + } + }; + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(new byte[] {0x02}); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(headers); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockUrl.openConnection()).thenReturn(mockConnection); + InputStream mockInputStream = mock(InputStream.class); + when(mockConnection.getInputStream()).thenReturn(mockInputStream); + when(mockBufferedReader.readLine()) + .thenAnswer( + new Answer() { + private int count = 0; + + public String answer(InvocationOnMock invocation) { + if (count++ == 3) { + return null; + } + return "*"; + } + }); + + // Execute + String resp = httpRequestManager.request(request); + + // Validation + mockedURLFactory.verify(() -> HttpRequestManager.URLFactory.create("1")); + // Verify setting of basic request properties + verify(mockConnection, times(1)).setConnectTimeout(5000); + verify(mockConnection, times(1)).setRequestMethod("post"); + // Verify header is being set + verify(mockConnection, times(1)).setRequestProperty("3", "3"); + // Verify request body is set + verify(mockConnection, times(1)).setFixedLengthStreamingMode(1); + verify(mockConnection, times(1)).setDoOutput(true); + verify(mockBufferedOutputStream, times(1)).write(new byte[] {0x02}, 0, 1); + verify(mockBufferedOutputStream, times(1)).flush(); + // Verify response body is being collected and returned + mockedInputStreamReaderFactory.verify( + () -> HttpRequestManager.InputStreamReaderFactory.create(mockInputStream)); + mockedBufferedReaderFactory.verify( + () -> HttpRequestManager.BufferedReaderFactory.create(mockInputStreamReader)); + verify(mockBufferedReader, times(4)).readLine(); + assertEquals("***", resp); + // Verify cleanup + verify(mockConnection, times(1)).disconnect(); + } + + @Test + public void request_shouldNotSetHeadersWhenNoneAreProvided() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(new byte[] {0x02}); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(Collections.emptyMap()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockUrl.openConnection()).thenReturn(mockConnection); + + // Execute + httpRequestManager.request(request); + + // Validation + verify(mockConnection, never()).setRequestProperty(anyString(), anyString()); + } + + @Test + public void request_shouldNotSetBodyWhenNoneIsProvided() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(null); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(Collections.emptyMap()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockUrl.openConnection()).thenReturn(mockConnection); + + // Execute + httpRequestManager.request(request); + + // Validation + verify(mockConnection, never()).setFixedLengthStreamingMode(anyInt()); + verify(mockConnection, never()).setDoOutput(anyBoolean()); + verify(mockBufferedOutputStream, never()).write(any(), anyInt(), anyInt()); + verify(mockBufferedOutputStream, never()).flush(); + } + + @Test + public void requestAsync_shouldScheduleRequest() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(null); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(Collections.emptyMap()); + HttpRequestCallback mockCallback = mock(HttpRequestCallback.class); + + // Execute + httpRequestManager.requestAsync(request, mockCallback); + + // Validation + verify(mockExecutor, times(1)).execute(any()); + } + + @Test + public void requestAsync_shouldCallOnCompleteCallbackOnSuccess() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(null); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(Collections.emptyMap()); + HttpRequestCallback mockCallback = mock(HttpRequestCallback.class); + doAnswer( + (Answer) + invocationOnMock -> { + Runnable runnable = invocationOnMock.getArgument(0, Runnable.class); + runnable.run(); + return null; + }) + .when(mockExecutor) + .execute(any()); + doAnswer( + (Answer) + invocationOnMock -> { + Runnable runnable = invocationOnMock.getArgument(0, Runnable.class); + runnable.run(); + return null; + }) + .when(mockHandler) + .post(any()); + doReturn("RESPONSE").when(httpRequestManager).request(any()); + + // Execute + httpRequestManager.requestAsync(request, mockCallback); + + // Validation + verify(mockHandler, times(1)).post(any()); + verify(mockCallback, never()).onError(any()); + verify(mockCallback, times(1)).onComplete("RESPONSE"); + } + + @Test + public void requestAsync_shouldCallOnErrorCallbackOnIOException() throws IOException { + // Preparation + WebViewRequest request = mock(WebViewRequest.class); + when(request.getUri()).thenReturn("1"); + when(request.getBody()).thenReturn(null); + when(request.getMethod()).thenReturn(WebViewLoadMethod.POST); + when(request.getHeaders()).thenReturn(Collections.emptyMap()); + HttpRequestCallback mockCallback = mock(HttpRequestCallback.class); + doAnswer( + (Answer) + invocationOnMock -> { + Runnable runnable = invocationOnMock.getArgument(0, Runnable.class); + runnable.run(); + return null; + }) + .when(mockExecutor) + .execute(any()); + doAnswer( + (Answer) + invocationOnMock -> { + Runnable runnable = invocationOnMock.getArgument(0, Runnable.class); + runnable.run(); + return null; + }) + .when(mockHandler) + .post(any()); + IOException exception = new IOException(); + doThrow(exception).when(httpRequestManager).request(any()); + + // Execute + httpRequestManager.requestAsync(request, mockCallback); + + // Validation + verify(mockHandler, times(1)).post(any()); + verify(mockCallback, never()).onComplete(any()); + verify(mockCallback, times(1)).onError(exception); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewRequestTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewRequestTest.java new file mode 100644 index 000000000000..c3ec42151dc3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewRequestTest.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class WebViewRequestTest { + + @Test + public void webViewLoadMethod_serialize_shouldReturnValue() { + assertEquals("get", WebViewLoadMethod.GET.serialize()); + assertEquals("post", WebViewLoadMethod.POST.serialize()); + } + + @Test + public void webViewLoadMethod_deserialize_shouldReturnEnumValue() { + assertEquals(WebViewLoadMethod.GET, WebViewLoadMethod.deserialize("get")); + assertEquals(WebViewLoadMethod.POST, WebViewLoadMethod.deserialize("post")); + } + + @Test(expected = IllegalArgumentException.class) + public void webViewLoadMethod_deserialize_shouldThrowIllegalArgumentExceptionForUnknownValue() { + WebViewLoadMethod.deserialize("fakeMethod"); + } + + @Test + public void webViewRequest_shouldConstructWithGivenParams() { + Map headers = + new HashMap() { + { + put("3", "3"); + } + }; + byte[] body = {0x04}; + WebViewRequest req = new WebViewRequest("1", WebViewLoadMethod.POST, headers, body); + + assertEquals(req.getUri(), "1"); + assertEquals(req.getMethod(), WebViewLoadMethod.POST); + assertEquals(req.getHeaders(), headers); + assertEquals(req.getBody(), body); + } + + @Test + public void webViewRequest_shouldConstructFromMap() { + final Map headers = + new HashMap() { + { + put("3", "3"); + } + }; + final byte[] body = {0x04}; + WebViewRequest req = + WebViewRequest.fromMap( + new HashMap() { + { + put("url", "1"); + put("method", "post"); + put("headers", headers); + put("body", body); + } + }); + + assertEquals(req.getUri(), "1"); + assertEquals(req.getMethod(), WebViewLoadMethod.POST); + assertEquals(req.getHeaders(), headers); + assertEquals(req.getBody(), body); + } +} 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 a22d165a6ff3..24b1abe97191 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/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_android/webview_surface_android.dart'; @@ -116,6 +117,7 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + doPostRequest, } class _SampleMenu extends StatelessWidget { @@ -153,6 +155,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -185,6 +190,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_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index b43507447462..503bbcf24ba6 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 @@ -392,6 +392,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/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index ac208a09ebc0..6f43bb795dbc 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.2.0 + webview_flutter_platform_interface: ^1.3.0 dev_dependencies: flutter_driver: 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..84166d95a2aa 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 = ""; }; @@ -278,13 +277,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; }; @@ -482,6 +484,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; @@ -496,6 +499,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; @@ -616,6 +620,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -639,6 +644,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -660,6 +666,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; @@ -673,6 +680,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; 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..9e0b1a7b36a9 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 @@ -301,4 +301,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..336bde2abd57 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,13 @@ NS_ASSUME_NONNULL_BEGIN - (UIView*)view; - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; + +- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments; + +- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result; + +- (void)onLoadRequest:(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..b56ff267410a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -116,7 +116,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 +141,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 +192,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 +487,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/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 5176adb9749c..2988f1da66c7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -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 ab620894d65ac4eb7ebcec77af00a7b93f93d548 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 5 Nov 2021 16:23:09 +0100 Subject: [PATCH 07/10] Revert ios project file additions --- .../example/ios/Runner.xcodeproj/project.pbxproj | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) 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 84166d95a2aa..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 @@ -193,6 +193,7 @@ C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, ); + name = Pods; path = Pods; sourceTree = ""; }; @@ -277,16 +278,13 @@ 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; }; @@ -484,7 +482,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; @@ -499,7 +496,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; @@ -620,7 +616,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -644,7 +639,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -666,7 +660,6 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; @@ -680,7 +673,6 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7624MWN53C; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; From 3ab45a974a0b54eaea4455504ff2d82d24622f86 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 5 Nov 2021 16:30:24 +0100 Subject: [PATCH 08/10] Fix comment --- .../io/flutter/plugins/webviewflutter/HttpRequestManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java index da715a802b4d..ab08455a58ae 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/HttpRequestManager.java @@ -93,7 +93,7 @@ public String request(WebViewRequest request) throws IOException { // because otherwise HTTPUrlConnection will be forced to buffer the complete // request body in memory before it is transmitted, wasting (and possibly exhausting) // heap and increasing latency. - // httpURLConnection.setFixedLengthStreamingMode(request.getBody().length); + httpURLConnection.setFixedLengthStreamingMode(request.getBody().length); httpURLConnection.setDoOutput(true); OutputStream os = BufferedOutputStreamFactory.create(httpURLConnection.getOutputStream()); From 7933e494040548908d5d615e1f51c5d6f6665e30 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Fri, 5 Nov 2021 16:37:41 +0100 Subject: [PATCH 09/10] Revert iOS changes to move to a separate PR --- .../example/ios/RunnerTests/FLTWebViewTests.m | 192 ------------------ .../example/lib/main.dart | 20 -- .../example/lib/web_view.dart | 5 - .../ios/Classes/FlutterWebView.h | 7 - .../ios/Classes/FlutterWebView.m | 90 +++----- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 6 files changed, 27 insertions(+), 289 deletions(-) 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 9e0b1a7b36a9..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 @@ -301,196 +301,4 @@ - (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 ea95cddf9fc9..21240f63ec1a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -6,7 +6,6 @@ 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'; @@ -121,7 +120,6 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, - doPostRequest, } class _SampleMenu extends StatelessWidget { @@ -159,9 +157,6 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; - case _MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; } }, itemBuilder: (BuildContext context) => >[ @@ -194,10 +189,6 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.doPostRequest, - child: Text('Post Request'), - ), ], ); }, @@ -268,17 +259,6 @@ 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 b2555cd831c6..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 @@ -322,11 +322,6 @@ 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 336bde2abd57..db52124d6a7c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -27,13 +27,6 @@ NS_ASSUME_NONNULL_BEGIN - (UIView*)view; - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; - -- (NSURLRequest*)buildNSURLRequest:(NSDictionary*)arguments; - -- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result; - -- (void)onLoadRequest:(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 b56ff267410a..5e12f8acb2ea 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -116,11 +116,7 @@ - (instancetype)initWithFrame:(CGRect)frame NSString* initialUrl = args[@"initialUrl"]; if ([initialUrl isKindOfClass:[NSString class]]) { - NSURL* url = [NSURL URLWithString:initialUrl]; - if (url) { - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; - [_webView loadRequest:request]; - } + [self loadUrl:initialUrl]; } } return self; @@ -141,8 +137,6 @@ - (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"]) { @@ -192,34 +186,12 @@ - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { } - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - 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) { + if (![self loadRequest:[call arguments]]) { 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); } } @@ -487,47 +459,37 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy } } -/** - * 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* urlString = requestParameters[@"uri"]; - if (!urlString) { - return nil; +- (bool)loadRequest:(NSDictionary*)request { + if (!request) { + return false; } - NSURL* url = [NSURL URLWithString:urlString]; - if (!url) { - 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]; + } } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; - - NSString* httpMethod = requestParameters[@"method"]; - if (httpMethod) { - [request setHTTPMethod:httpMethod]; - } + return false; +} - id httpBody = requestParameters[@"body"]; - if ([httpBody isKindOfClass:[FlutterStandardTypedData class]]) { - [request setHTTPBody:[httpBody data]]; - } +- (bool)loadUrl:(NSString*)url { + return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; +} - id headers = requestParameters[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - [request setAllHTTPHeaderFields:headers]; +- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { + NSURL* nsUrl = [NSURL URLWithString:url]; + if (!nsUrl) { + return false; } - - return request; + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; + [request setAllHTTPHeaderFields:headers]; + [_webView loadRequest:request]; + return true; } - (void)registerJavaScriptChannels:(NSSet*)channelNames diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 2988f1da66c7..5176adb9749c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.3.0 + webview_flutter_platform_interface: ^1.2.0 dev_dependencies: flutter_driver: From 95b29c3e8c453acc97a2d0a3dcb674e6469ae2c6 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 1 Dec 2021 13:43:18 +0100 Subject: [PATCH 10/10] Fix merge issue --- .../webview_platform_controller.dart | 19 ------------------- 1 file changed, 19 deletions(-) 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 194a32747153..da73204e0099 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 @@ -87,25 +87,6 @@ abstract class WebViewPlatformController { 'WebView loadRequest 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.