Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WIP Native Android & iOS implementations for loadRequest method.
Co-authored-by: Yusuf <[email protected]>
  • Loading branch information
BeMacized and ydag committed Nov 5, 2021
commit 3f27a3a058c23f87a0cda675ad945b6ca57c90d3
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {

Expand All @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> request = (Map<String, Object>) methodCall.arguments;
if (request == null) {
return null;
}

Map<String, Object> requestObject = (Map<String, Object>) request.get("request");
if (requestObject == null) {
return null;
}

return WebViewRequest.fromMap(requestObject);
}

private void canGoBack(Result result) {
result.success(webView.canGoBack());
}
Expand Down Expand Up @@ -492,4 +565,22 @@ public void dispose() {
}
webView.destroy();
}

/** Factory class for creating a {@link HttpRequestManager} */
static class HttpRequestManagerFactory {
/**
* Creates a {@link HttpRequestManager}.
*
* <p><strong>Important:</strong> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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 <a
* href="https://developer.android.com/guide/background/threading">https://developer.android.com/guide/background/threading</a>.
*
* @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<String, String> 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}.
*
* <p><strong>Important:</strong> 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}.
*
* <p><strong>Important:</strong> 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}.
*
* <p><strong>Important:</strong> 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}.
*
* <p><strong>Important:</strong> 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);
}
}
}
Loading