Commit 3ffa1c0a authored by Lorenzo Pichilli's avatar Lorenzo Pichilli

Updated onLoadResource event, Updated WebResourceResponse class, Deleted...

Updated onLoadResource event, Updated WebResourceResponse class, Deleted WebResourceRequest class, added useOnDownloadStart option, created webview options classes, added initial content blocking support
parent 49983cfd
This diff is collapsed.
...@@ -9,9 +9,17 @@ ...@@ -9,9 +9,17 @@
- Merge "migrating to swift 5.0" [#162](https://github.com/pichillilorenzo/flutter_inappbrowser/pull/162) (thanks to [fattiger00](https://github.com/fattiger00)) - Merge "migrating to swift 5.0" [#162](https://github.com/pichillilorenzo/flutter_inappbrowser/pull/162) (thanks to [fattiger00](https://github.com/fattiger00))
- Merge "Update readme example" [#178](https://github.com/pichillilorenzo/flutter_inappbrowser/pull/178) (thanks to [SebastienBtr](https://github.com/SebastienBtr)) - Merge "Update readme example" [#178](https://github.com/pichillilorenzo/flutter_inappbrowser/pull/178) (thanks to [SebastienBtr](https://github.com/SebastienBtr))
- Added `horizontalScrollBarEnabled` and `verticalScrollBarEnabled` options to enable/disable the corresponding scrollbar of the WebView [#165](https://github.com/pichillilorenzo/flutter_inappbrowser/issues/165) - Added `horizontalScrollBarEnabled` and `verticalScrollBarEnabled` options to enable/disable the corresponding scrollbar of the WebView [#165](https://github.com/pichillilorenzo/flutter_inappbrowser/issues/165)
- Added `onDownloadStart` event: event fires when the WebView recognizes and starts a downloadable file. - Added `onDownloadStart` event and `useOnDownloadStart` option: event fires when the WebView recognizes and starts a downloadable file.
- Added `onLoadResourceCustomScheme` event and `resourceCustomSchemes` option to set custom schemes that WebView must handle to load resources - Added `onLoadResourceCustomScheme` event and `resourceCustomSchemes` option to set custom schemes that WebView must handle to load resources
- Added `onTargetBlank` event and `useOnTargetBlank` option to manage links with `target="_blank"` - Added `onTargetBlank` event and `useOnTargetBlank` option to manage links with `target="_blank"`
- Added `ContentBlocker`, `ContentBlockerTrigger` and `ContentBlockerAction` classes and the `contentBlockers` option that allows to define a set of rules to use to block content in the WebView
### BREAKING CHANGES
- Deleted `WebResourceRequest` class
- Updated `WebResourceResponse` class
- Updated `onLoadResource` event
- WebView options are now available with the new corresponding classes: `InAppWebViewOptions`, `AndroidInAppWebViewOptions`, `iOSInAppWebViewOptions`, `InAppBrowserOptions`, `AndroidInAppBrowserOptions`, `iOSInAppBrowserOptions`, `AndroidChromeCustomTabsOptions` and `iOSChromeCustomTabsOptions`
## 1.2.1 ## 1.2.1
......
package com.pichillilorenzo.flutter_inappbrowser.ContentBlocker;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.webkit.WebResourceResponse;
import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InAppWebView;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.Request;
import okhttp3.Response;
public class ContentBlocker {
protected static final String LOG_TAG = "ContentBlocker";
public static WebResourceResponse checkUrl(final InAppWebView webView, String url, ContentBlockerTriggerResourceType responseResourceType) {
if (webView.options.contentBlockers == null)
return null;
for (Map<String, Map<String, Object>> contentBlocker : webView.options.contentBlockers) {
ContentBlockerTrigger trigger = ContentBlockerTrigger.fromMap(contentBlocker.get("trigger"));
List<ContentBlockerTriggerResourceType> resourceTypes = trigger.resourceType;
ContentBlockerAction action = ContentBlockerAction.fromMap(contentBlocker.get("action"));
Pattern mPattern = Pattern.compile(trigger.urlFilter);
Matcher m = mPattern.matcher(url);
if (m.matches()) {
Log.d(LOG_TAG, url);
Log.d(LOG_TAG, responseResourceType.toString());
Log.d(LOG_TAG, resourceTypes.toString());
if (resourceTypes != null && resourceTypes.size() > 0 && !resourceTypes.contains(responseResourceType)) {
return null;
}
switch (action.type) {
case BLOCK:
return new WebResourceResponse("", "", null);
case CSS_DISPLAY_NONE:
final String jsScript = "function hide () { document.querySelectorAll('" + action.selector + "').forEach(function (item, index) { item.style.display = \"none\"; }); }; hide(); document.addEventListener(\"DOMContentLoaded\", function(event) { hide(); });";
final Handler handler = new Handler(Looper.getMainLooper());
Log.d(LOG_TAG, jsScript);
handler.post(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(jsScript, null);
} else {
webView.loadUrl("javascript:" + jsScript);
}
}
});
break;
case MAKE_HTTPS:
if (url.startsWith("http://")) {
String urlHttps = url.replace("http://", "https://");
Request mRequest = new Request.Builder().url(urlHttps).build();
Response response = null;
try {
response = webView.httpClient.newCall(mRequest).execute();
byte[] dataBytes = response.body().bytes();
InputStream dataStream = new ByteArrayInputStream(dataBytes);
String[] contentTypeSplitted = response.header("content-type", "text/plain").split(";");
String contentType = contentTypeSplitted[0].trim();
String encoding = (contentTypeSplitted.length > 1 && contentTypeSplitted[1].contains("charset="))
? contentTypeSplitted[1].replace("charset=", "").trim()
: "utf-8";
response.close();
return new WebResourceResponse(contentType, encoding, dataStream);
} catch (IOException e) {
e.printStackTrace();
if (response != null) {
response.close();
}
Log.e(LOG_TAG, e.getMessage());
}
}
break;
}
}
}
return null;
}
public static WebResourceResponse checkUrl(final InAppWebView webView, String url) {
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromUrl(webView, url);
return checkUrl(webView, url, responseResourceType);
}
public static WebResourceResponse checkUrl(final InAppWebView webView, String url, String contentType) {
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromContentType(contentType);
return checkUrl(webView, url, responseResourceType);
}
public static ContentBlockerTriggerResourceType getResourceTypeFromUrl(InAppWebView webView, String url) {
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
// make an HTTP "HEAD" request to the server for that URL. This will not return the full content of the URL.
if (url.startsWith("http://") || url.startsWith("https://")) {
Request mRequest = new Request.Builder().url(url).head().build();
Response response = null;
try {
response = webView.httpClient.newCall(mRequest).execute();
if (response.header("content-type") != null) {
String[] contentTypeSplitted = response.header("content-type").split(";");
String contentType = contentTypeSplitted[0].trim();
String encoding = (contentTypeSplitted.length > 1 && contentTypeSplitted[1].contains("charset="))
? contentTypeSplitted[1].replace("charset=", "").trim()
: "utf-8";
response.close();
responseResourceType = getResourceTypeFromContentType(contentType);
}
} catch (IOException e) {
if (response != null) {
response.close();
}
e.printStackTrace();
}
}
return responseResourceType;
}
public static ContentBlockerTriggerResourceType getResourceTypeFromContentType(String contentType) {
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
if (contentType.equals("text/css")) {
responseResourceType = ContentBlockerTriggerResourceType.STYLE_SHEET;
} else if (contentType.equals("image/svg+xml")) {
responseResourceType = ContentBlockerTriggerResourceType.SVG_DOCUMENT;
} else if (contentType.startsWith("image/")) {
responseResourceType = ContentBlockerTriggerResourceType.IMAGE;
} else if (contentType.startsWith("font/")) {
responseResourceType = ContentBlockerTriggerResourceType.FONT;
} else if (contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.equals("application/ogg")) {
responseResourceType = ContentBlockerTriggerResourceType.MEDIA;
} else if (contentType.endsWith("javascript")) {
responseResourceType = ContentBlockerTriggerResourceType.SCRIPT;
} else if (contentType.startsWith("text/")) {
responseResourceType = ContentBlockerTriggerResourceType.DOCUMENT;
}
return responseResourceType;
}
}
package com.pichillilorenzo.flutter_inappbrowser.ContentBlocker;
import java.util.Map;
public class ContentBlockerAction {
ContentBlockerActionType type;
String selector;
ContentBlockerAction(ContentBlockerActionType type, String selector) {
this.type = type;
if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) {
assert(selector != null);
}
this.selector = selector;
}
public static ContentBlockerAction fromMap(Map<String, Object> map) {
ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type"));
String selector = (String) map.get("selector");
return new ContentBlockerAction(type, selector);
}
}
package com.pichillilorenzo.flutter_inappbrowser.ContentBlocker;
public enum ContentBlockerActionType {
BLOCK ("block"),
CSS_DISPLAY_NONE ("css-display-none"),
MAKE_HTTPS ("make-https");
private final String value;
private ContentBlockerActionType(String value) {
this.value = value;
}
public boolean equalsValue(String otherValue) {
return value.equals(otherValue);
}
public static ContentBlockerActionType fromValue(String value) {
for( ContentBlockerActionType type : ContentBlockerActionType.values()) {
if(value.equals(type.value))
return type;
}
throw new IllegalArgumentException("No enum constant: " + value);
}
public String toString() {
return this.value;
}
}
package com.pichillilorenzo.flutter_inappbrowser.ContentBlocker;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ContentBlockerTrigger {
public String urlFilter;
List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
public ContentBlockerTrigger(String urlFilter, List<ContentBlockerTriggerResourceType> resourceType) {
this.urlFilter = urlFilter;
this.resourceType = resourceType != null ? resourceType : this.resourceType;
}
public static ContentBlockerTrigger fromMap(Map<String, Object> map) {
String urlFilter = (String) map.get("url-filter");
List<String> resourceTypeStringList = (List<String>) map.get("resource-type");
List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
for (String type : resourceTypeStringList) {
resourceType.add(ContentBlockerTriggerResourceType.fromValue(type));
}
return new ContentBlockerTrigger(urlFilter, resourceType);
}
}
package com.pichillilorenzo.flutter_inappbrowser.ContentBlocker;
public enum ContentBlockerTriggerResourceType {
DOCUMENT ("document"),
IMAGE ("image"),
STYLE_SHEET ("style-sheet"),
SCRIPT ("script"),
FONT ("font"),
SVG_DOCUMENT ("svg-document"),
MEDIA ("media"),
RAW ("raw");
private final String value;
private ContentBlockerTriggerResourceType(String value) {
this.value = value;
}
public boolean equalsValue(String otherValue) {
return value.equals(otherValue);
}
public static ContentBlockerTriggerResourceType fromValue(String value) {
for( ContentBlockerTriggerResourceType type : ContentBlockerTriggerResourceType.values()) {
if(value.equals(type.value))
return type;
}
throw new IllegalArgumentException("No enum constant: " + value);
}
public String toString() {
return this.value;
}
}
package com.pichillilorenzo.flutter_inappbrowser.InAppWebView; package com.pichillilorenzo.flutter_inappbrowser.InAppWebView;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
...@@ -23,7 +22,6 @@ import com.pichillilorenzo.flutter_inappbrowser.FlutterWebView; ...@@ -23,7 +22,6 @@ import com.pichillilorenzo.flutter_inappbrowser.FlutterWebView;
import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserActivity; import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserActivity;
import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserFlutterPlugin; import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserFlutterPlugin;
import com.pichillilorenzo.flutter_inappbrowser.JavaScriptBridgeInterface; import com.pichillilorenzo.flutter_inappbrowser.JavaScriptBridgeInterface;
import com.pichillilorenzo.flutter_inappbrowser.RequestPermissionHandler;
import com.pichillilorenzo.flutter_inappbrowser.Util; import com.pichillilorenzo.flutter_inappbrowser.Util;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
...@@ -47,11 +45,11 @@ public class InAppWebView extends WebView { ...@@ -47,11 +45,11 @@ public class InAppWebView extends WebView {
public InAppBrowserActivity inAppBrowserActivity; public InAppBrowserActivity inAppBrowserActivity;
public FlutterWebView flutterWebView; public FlutterWebView flutterWebView;
public int id; public int id;
InAppWebViewClient inAppWebViewClient; public InAppWebViewClient inAppWebViewClient;
InAppWebChromeClient inAppWebChromeClient; public InAppWebChromeClient inAppWebChromeClient;
public InAppWebViewOptions options; public InAppWebViewOptions options;
public boolean isLoading = false; public boolean isLoading = false;
OkHttpClient httpClient; public OkHttpClient httpClient;
int okHttpClientCacheSize = 10 * 1024 * 1024; // 10MB int okHttpClientCacheSize = 10 * 1024 * 1024; // 10MB
static final String consoleLogJS = "(function() {" + static final String consoleLogJS = "(function() {" +
...@@ -80,6 +78,15 @@ public class InAppWebView extends WebView { ...@@ -80,6 +78,15 @@ public class InAppWebView extends WebView {
" }" + " }" +
"})();"; "})();";
static final String resourceObserverJS = "(function() {" +
" var observer = new PerformanceObserver(function(list) {" +
" list.getEntries().forEach(function(entry) {" +
" window." + JavaScriptBridgeInterface.name + "._resourceLoaded(JSON.stringify(entry));" +
" });" +
" });" +
" observer.observe({entryTypes: ['resource']});" +
"})();";
static final String platformReadyJS = "window.dispatchEvent(new Event('flutterInAppBrowserPlatformReady'));"; static final String platformReadyJS = "window.dispatchEvent(new Event('flutterInAppBrowserPlatformReady'));";
public InAppWebView(Context context) { public InAppWebView(Context context) {
...@@ -117,7 +124,8 @@ public class InAppWebView extends WebView { ...@@ -117,7 +124,8 @@ public class InAppWebView extends WebView {
boolean isFromInAppBrowserActivity = inAppBrowserActivity != null; boolean isFromInAppBrowserActivity = inAppBrowserActivity != null;
httpClient = new OkHttpClient().newBuilder().cache(new Cache(getContext().getCacheDir(), okHttpClientCacheSize)).build(); //httpClient = new OkHttpClient().newBuilder().cache(new Cache(getContext().getCacheDir(), okHttpClientCacheSize)).build();
httpClient = new OkHttpClient().newBuilder().build();
addJavascriptInterface(new JavaScriptBridgeInterface((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView), JavaScriptBridgeInterface.name); addJavascriptInterface(new JavaScriptBridgeInterface((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView), JavaScriptBridgeInterface.name);
...@@ -127,23 +135,8 @@ public class InAppWebView extends WebView { ...@@ -127,23 +135,8 @@ public class InAppWebView extends WebView {
inAppWebViewClient = new InAppWebViewClient((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView); inAppWebViewClient = new InAppWebViewClient((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView);
setWebViewClient(inAppWebViewClient); setWebViewClient(inAppWebViewClient);
setDownloadListener(new DownloadListener() { if (options.useOnDownloadStart)
@Override setDownloadListener(new DownloadStartListener());
public void onDownloadStart(final String url, final String userAgent,
final String contentDisposition, final String mimetype,
final long contentLength) {
RequestPermissionHandler.checkAndRun(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, RequestPermissionHandler.REQUEST_CODE_WRITE_EXTERNAL_STORAGE, new Runnable(){
@Override
public void run(){
Map<String, Object> obj = new HashMap<>();
if (inAppBrowserActivity != null)
obj.put("uuid", inAppBrowserActivity.uuid);
obj.put("url", url);
getChannel().invokeMethod("onDownloadStart", obj);
}
});
}
});
WebSettings settings = getSettings(); WebSettings settings = getSettings();
...@@ -396,6 +389,14 @@ public class InAppWebView extends WebView { ...@@ -396,6 +389,14 @@ public class InAppWebView extends WebView {
if (newOptionsMap.get("useOnTargetBlank") != null && options.useOnTargetBlank != newOptions.useOnTargetBlank) if (newOptionsMap.get("useOnTargetBlank") != null && options.useOnTargetBlank != newOptions.useOnTargetBlank)
settings.setSupportMultipleWindows(newOptions.useOnTargetBlank); settings.setSupportMultipleWindows(newOptions.useOnTargetBlank);
if (newOptionsMap.get("useOnDownloadStart") != null && options.useOnDownloadStart != newOptions.useOnDownloadStart) {
if (newOptions.useOnDownloadStart) {
setDownloadListener(new DownloadStartListener());
} else {
setDownloadListener(null);
}
}
options = newOptions; options = newOptions;
} }
...@@ -534,6 +535,17 @@ public class InAppWebView extends WebView { ...@@ -534,6 +535,17 @@ public class InAppWebView extends WebView {
return (inAppBrowserActivity != null) ? InAppBrowserFlutterPlugin.instance.channel : flutterWebView.channel; return (inAppBrowserActivity != null) ? InAppBrowserFlutterPlugin.instance.channel : flutterWebView.channel;
} }
class DownloadStartListener implements DownloadListener {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Map<String, Object> obj = new HashMap<>();
if (inAppBrowserActivity != null)
obj.put("uuid", inAppBrowserActivity.uuid);
obj.put("url", url);
getChannel().invokeMethod("onDownloadStart", obj);
}
}
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
......
...@@ -5,10 +5,6 @@ import android.graphics.Bitmap; ...@@ -5,10 +5,6 @@ import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.net.http.SslError; import android.net.http.SslError;
import android.os.Build; import android.os.Build;
import androidx.annotation.RequiresApi;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.webkit.CookieManager; import android.webkit.CookieManager;
...@@ -21,6 +17,9 @@ import android.webkit.WebResourceResponse; ...@@ -21,6 +17,9 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappbrowser.ContentBlocker.ContentBlocker;
import com.pichillilorenzo.flutter_inappbrowser.FlutterWebView; import com.pichillilorenzo.flutter_inappbrowser.FlutterWebView;
import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserActivity; import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserActivity;
import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserFlutterPlugin; import com.pichillilorenzo.flutter_inappbrowser.InAppBrowserFlutterPlugin;
...@@ -28,15 +27,10 @@ import com.pichillilorenzo.flutter_inappbrowser.JavaScriptBridgeInterface; ...@@ -28,15 +27,10 @@ import com.pichillilorenzo.flutter_inappbrowser.JavaScriptBridgeInterface;
import com.pichillilorenzo.flutter_inappbrowser.Util; import com.pichillilorenzo.flutter_inappbrowser.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import okhttp3.Request;
import okhttp3.Response;
public class InAppWebViewClient extends WebViewClient { public class InAppWebViewClient extends WebViewClient {
...@@ -185,12 +179,16 @@ public class InAppWebViewClient extends WebViewClient { ...@@ -185,12 +179,16 @@ public class InAppWebViewClient extends WebViewClient {
*/ */
@Override @Override
public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
startPageTime = System.currentTimeMillis(); InAppWebView webView = (InAppWebView) view;
if (webView.options.useOnLoadResource)
webView.loadUrl("javascript:" + webView.resourceObserverJS.replaceAll("[\r\n]+", ""));
((inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView).isLoading = true; super.onPageStarted(view, url, favicon);
startPageTime = System.currentTimeMillis();
webView.isLoading = true;
if (inAppBrowserActivity != null && inAppBrowserActivity.searchView != null && !url.equals(inAppBrowserActivity.searchView.getQuery().toString())) { if (inAppBrowserActivity != null && inAppBrowserActivity.searchView != null && !url.equals(inAppBrowserActivity.searchView.getQuery().toString())) {
inAppBrowserActivity.searchView.setQuery(url, false); inAppBrowserActivity.searchView.setQuery(url, false);
} }
...@@ -204,9 +202,11 @@ public class InAppWebViewClient extends WebViewClient { ...@@ -204,9 +202,11 @@ public class InAppWebViewClient extends WebViewClient {
public void onPageFinished(final WebView view, String url) { public void onPageFinished(final WebView view, String url) {
InAppWebView webView = (InAppWebView) view;
super.onPageFinished(view, url); super.onPageFinished(view, url);
((inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView).isLoading = false; webView.isLoading = false;
// CB-10395 InAppBrowserFlutterPlugin's WebView not storing cookies reliable to local device storage // CB-10395 InAppBrowserFlutterPlugin's WebView not storing cookies reliable to local device storage
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
...@@ -305,7 +305,7 @@ public class InAppWebViewClient extends WebViewClient { ...@@ -305,7 +305,7 @@ public class InAppWebViewClient extends WebViewClient {
@Override @Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
InAppWebView webView = (InAppWebView) view; final InAppWebView webView = (InAppWebView) view;
final String url = request.getUrl().toString(); final String url = request.getUrl().toString();
String scheme = request.getUrl().getScheme(); String scheme = request.getUrl().getScheme();
...@@ -324,105 +324,16 @@ public class InAppWebViewClient extends WebViewClient { ...@@ -324,105 +324,16 @@ public class InAppWebViewClient extends WebViewClient {
} }
else if (flutterResult.result != null) { else if (flutterResult.result != null) {
Map<String, String> res = (Map<String, String>) flutterResult.result; Map<String, String> res = (Map<String, String>) flutterResult.result;
WebResourceResponse response = ContentBlocker.checkUrl(webView, url, res.get("content-type"));
if (response != null)
return response;
byte[] data = Base64.decode(res.get("base64data"), Base64.DEFAULT); byte[] data = Base64.decode(res.get("base64data"), Base64.DEFAULT);
return new WebResourceResponse(res.get("content-type"), res.get("content-encoding"), new ByteArrayInputStream(data)); return new WebResourceResponse(res.get("content-type"), res.get("content-encoding"), new ByteArrayInputStream(data));
} }
} }
if (!request.getMethod().toLowerCase().equals("get") || WebResourceResponse response = ContentBlocker.checkUrl(webView, url);
!(((inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView).options.useOnLoadResource)) { return response;
return null;
}
try {
Request mRequest = new Request.Builder().url(url).build();
long startResourceTime = System.currentTimeMillis();
Response response = ((inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView).httpClient.newCall(mRequest).execute();
long startTime = startResourceTime - startPageTime;
startTime = (startTime < 0) ? 0 : startTime;
long duration = System.currentTimeMillis() - startResourceTime;
if (response.cacheResponse() != null) {
duration = 0;
}
String reasonPhrase = response.message();
if (reasonPhrase.equals("")) {
reasonPhrase = statusCodeMapping.get(response.code());
}
reasonPhrase = (reasonPhrase.equals("") || reasonPhrase == null) ? "OK" : reasonPhrase;
Map<String, String> headersResponse = new HashMap<String, String>();
for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
StringBuilder value = new StringBuilder();
for (String val : entry.getValue()) {
value.append((value.toString().isEmpty()) ? val : "; " + val);
}
headersResponse.put(entry.getKey().toLowerCase(), value.toString());
}
Map<String, String> headersRequest = new HashMap<String, String>();
for (Map.Entry<String, List<String>> entry : mRequest.headers().toMultimap().entrySet()) {
StringBuilder value = new StringBuilder();
for (String val : entry.getValue()) {
value.append((value.toString().isEmpty()) ? val : "; " + val);
}
headersRequest.put(entry.getKey().toLowerCase(), value.toString());
}
final Map<String, Object> obj = new HashMap<>();
Map<String, Object> res = new HashMap<>();
Map<String, Object> req = new HashMap<>();
if (inAppBrowserActivity != null)
obj.put("uuid", inAppBrowserActivity.uuid);
byte[] dataBytes = response.body().bytes();
InputStream dataStream = new ByteArrayInputStream(dataBytes);
res.put("url", url);
res.put("statusCode", response.code());
res.put("headers", headersResponse);
res.put("startTime", startTime);
res.put("duration", duration);
res.put("data", dataBytes);
req.put("url", url);
req.put("headers", headersRequest);
req.put("method", mRequest.method());
obj.put("response", res);
obj.put("request", req);
// java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread.
// https://github.com/pichillilorenzo/flutter_inappbrowser/issues/98
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
getChannel().invokeMethod("onLoadResource", obj);
}
});
// this return is not working (it blocks some resources), so return null
// return new WebResourceResponse(
// response.header("content-type", "text/plain").split(";")[0].trim(),
// response.header("content-encoding", "utf-8"),
// response.code(),
// reasonPhrase,
// headersResponse,
// dataStream
// );
} catch (IOException e) {
e.printStackTrace();
Log.d(LOG_TAG, e.getMessage());
} catch (Exception e) {
e.printStackTrace();
Log.d(LOG_TAG, e.getMessage());
}
return null;
} }
private MethodChannel getChannel() { private MethodChannel getChannel() {
......
...@@ -4,6 +4,7 @@ import com.pichillilorenzo.flutter_inappbrowser.Options; ...@@ -4,6 +4,7 @@ import com.pichillilorenzo.flutter_inappbrowser.Options;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
public class InAppWebViewOptions extends Options { public class InAppWebViewOptions extends Options {
...@@ -11,6 +12,7 @@ public class InAppWebViewOptions extends Options { ...@@ -11,6 +12,7 @@ public class InAppWebViewOptions extends Options {
public boolean useShouldOverrideUrlLoading = false; public boolean useShouldOverrideUrlLoading = false;
public boolean useOnLoadResource = false; public boolean useOnLoadResource = false;
public boolean useOnDownloadStart = false;
public boolean useOnTargetBlank = false; public boolean useOnTargetBlank = false;
public boolean clearCache = false; public boolean clearCache = false;
public String userAgent = ""; public String userAgent = "";
...@@ -21,6 +23,7 @@ public class InAppWebViewOptions extends Options { ...@@ -21,6 +23,7 @@ public class InAppWebViewOptions extends Options {
public boolean verticalScrollBarEnabled = true; public boolean verticalScrollBarEnabled = true;
public boolean horizontalScrollBarEnabled = true; public boolean horizontalScrollBarEnabled = true;
public List<String> resourceCustomSchemes = new ArrayList<>(); public List<String> resourceCustomSchemes = new ArrayList<>();
public List<Map<String, Map<String, Object>>> contentBlockers = new ArrayList<>();
public boolean clearSessionCache = false; public boolean clearSessionCache = false;
public boolean builtInZoomControls = false; public boolean builtInZoomControls = false;
......
...@@ -8,6 +8,11 @@ import android.webkit.JavascriptInterface; ...@@ -8,6 +8,11 @@ import android.webkit.JavascriptInterface;
import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InAppWebView; import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InAppWebView;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -77,7 +82,42 @@ public class JavaScriptBridgeInterface { ...@@ -77,7 +82,42 @@ public class JavaScriptBridgeInterface {
}); });
} }
}); });
}
@JavascriptInterface
public void _resourceLoaded(String json) {
try {
JSONObject jsonObject = new JSONObject(json);
final Map<String, Object> obj = new HashMap<>();
if (inAppBrowserActivity != null)
obj.put("uuid", inAppBrowserActivity.uuid);
String initiatorType = jsonObject.getString("initiatorType");
String url = jsonObject.getString("name");
Double startTime = jsonObject.getDouble("startTime");
Double duration = jsonObject.getDouble("duration");
obj.put("initiatorType", initiatorType);
obj.put("url", url);
obj.put("startTime", startTime);
obj.put("duration", duration);
// java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread.
// https://github.com/pichillilorenzo/flutter_inappbrowser/issues/98
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
getChannel().invokeMethod("onLoadResource", obj);
}
});
} catch (final JSONException e) {
Log.e(LOG_TAG, "Json parsing error: " + e.getMessage());
}
} }
private MethodChannel getChannel() { private MethodChannel getChannel() {
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
<application <application
android:name="com.pichillilorenzo.flutterwebviewexample.MyApplication" android:name="com.pichillilorenzo.flutterwebviewexample.MyApplication"
android:label="flutter_inappbrowser_example" android:label="flutter_inappbrowser_example"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Flutter InAppBrowser</title> <title>Flutter InAppBrowser</title>
<link rel="stylesheet" href="https://getbootstrap.com/docs/4.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="http://getbootstrap.com/docs/4.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head> </head>
......
...@@ -65,14 +65,21 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> { ...@@ -65,14 +65,21 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> {
//initialUrl: "https://flutter.dev/", //initialUrl: "https://flutter.dev/",
initialFile: "assets/index.html", initialFile: "assets/index.html",
initialHeaders: {}, initialHeaders: {},
initialOptions: { initialOptions: [
//"mediaPlaybackRequiresUserGesture": false, InAppWebViewOptions(
//"allowsInlineMediaPlayback": true, useShouldOverrideUrlLoading: true,
"useShouldOverrideUrlLoading": true, useOnTargetBlank: true,
"useOnTargetBlank": true, //useOnLoadResource: true,
"resourceCustomSchemes": ["my-special-custom-scheme"], useOnDownloadStart: true,
//"useOnLoadResource": true resourceCustomSchemes: ["my-special-custom-scheme"],
}, contentBlockers: [
ContentBlocker(
ContentBlockerTrigger(".*", resourceType: [ContentBlockerTriggerResourceType.IMAGE]),
ContentBlockerAction(ContentBlockerActionType.BLOCK)
)
]
)
],
onWebViewCreated: (InAppWebViewController controller) { onWebViewCreated: (InAppWebViewController controller) {
webView = controller; webView = controller;
...@@ -104,8 +111,8 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> { ...@@ -104,8 +111,8 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> {
print("override $url"); print("override $url");
controller.loadUrl(url); controller.loadUrl(url);
}, },
onLoadResource: (InAppWebViewController controller, WebResourceResponse response, WebResourceRequest request) { onLoadResource: (InAppWebViewController controller, WebResourceResponse response) {
print("Started at: " + print("Resource type: '"+response.initiatorType + "' started at: " +
response.startTime.toString() + response.startTime.toString() +
"ms ---> duration: " + "ms ---> duration: " +
response.duration.toString() + response.duration.toString() +
...@@ -113,13 +120,13 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> { ...@@ -113,13 +120,13 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> {
response.url); response.url);
}, },
onConsoleMessage: (InAppWebViewController controller, ConsoleMessage consoleMessage) { onConsoleMessage: (InAppWebViewController controller, ConsoleMessage consoleMessage) {
// print(""" print("""
// console output: console output:
// sourceURL: ${consoleMessage.sourceURL} sourceURL: ${consoleMessage.sourceURL}
// lineNumber: ${consoleMessage.lineNumber} lineNumber: ${consoleMessage.lineNumber}
// message: ${consoleMessage.message} message: ${consoleMessage.message}
// messageLevel: ${consoleMessage.messageLevel} messageLevel: ${consoleMessage.messageLevel}
// """); """);
}, },
onDownloadStart: (InAppWebViewController controller, String url) async { onDownloadStart: (InAppWebViewController controller, String url) async {
final taskId = await FlutterDownloader.enqueue( final taskId = await FlutterDownloader.enqueue(
...@@ -133,7 +140,7 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> { ...@@ -133,7 +140,7 @@ class _InlineExampleScreenState extends State<InlineExampleScreen> {
if (scheme == "my-special-custom-scheme") { if (scheme == "my-special-custom-scheme") {
var bytes = await rootBundle.load("assets/" + url.replaceFirst("my-special-custom-scheme://", "", 0)); var bytes = await rootBundle.load("assets/" + url.replaceFirst("my-special-custom-scheme://", "", 0));
var asBase64 = base64.encode(bytes.buffer.asUint8List()); var asBase64 = base64.encode(bytes.buffer.asUint8List());
var response = new CustomSchemeResponse(asBase64, "image/svg+xml", "utf-8"); var response = new CustomSchemeResponse(asBase64, "image/svg+xml", contentEnconding: "utf-8");
return response; return response;
} }
return null; return null;
......
...@@ -400,9 +400,7 @@ class _MyAppState extends State<MyApp> { ...@@ -400,9 +400,7 @@ class _MyAppState extends State<MyApp> {
initialHeaders: { initialHeaders: {
}, },
initialOptions: { initialOptions: [],
},
onWebViewCreated: (InAppWebViewController controller) { onWebViewCreated: (InAppWebViewController controller) {
webView = controller; webView = controller;
}, },
......
...@@ -47,7 +47,7 @@ class MyInappBrowser extends InAppBrowser { ...@@ -47,7 +47,7 @@ class MyInappBrowser extends InAppBrowser {
} }
@override @override
void onLoadResource(WebResourceResponse response, WebResourceRequest request) { void onLoadResource(WebResourceResponse response) {
print("Started at: " + print("Started at: " +
response.startTime.toString() + response.startTime.toString() +
"ms ---> duration: " + "ms ---> duration: " +
......
...@@ -63,7 +63,7 @@ let resourceObserverJS = """ ...@@ -63,7 +63,7 @@ let resourceObserverJS = """
window.webkit.messageHandlers['resourceLoaded'].postMessage(JSON.stringify(entry)); window.webkit.messageHandlers['resourceLoaded'].postMessage(JSON.stringify(entry));
}); });
}); });
observer.observe({entryTypes: ['resource', 'mark', 'measure']}); observer.observe({entryTypes: ['resource']});
})(); })();
""" """
...@@ -586,17 +586,18 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi ...@@ -586,17 +586,18 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
decidePolicyFor navigationResponse: WKNavigationResponse, decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
if (options?.useOnLoadResource)! { // if (options?.useOnLoadResource)! {
if let url = navigationResponse.response.url { // if let url = navigationResponse.response.url {
if WKNavigationMap[url.absoluteString] != nil { // if WKNavigationMap[url.absoluteString] != nil {
let startResourceTime: Int64 = (WKNavigationMap[url.absoluteString]!["startTime"] as! Int64) // let startResourceTime: Int64 = (WKNavigationMap[url.absoluteString]!["startTime"] as! Int64)
let startTime: Int64 = startResourceTime - startPageTime; // let startTime: Int64 = startResourceTime - startPageTime;
let duration: Int64 = currentTimeInMilliSeconds() - startResourceTime; // let duration: Int64 = currentTimeInMilliSeconds() - startResourceTime;
onLoadResource(response: navigationResponse.response, fromRequest: WKNavigationMap[url.absoluteString]!["request"] as? URLRequest, withData: Data(), startTime: startTime, duration: duration) // onLoadResource(response: navigationResponse.response, fromRequest: WKNavigationMap[url.absoluteString]!["request"] as? URLRequest, withData: Data(), startTime: startTime, duration: duration)
} // }
} // }
} // }
if (options?.useOnDownloadStart)! {
let mimeType = navigationResponse.response.mimeType let mimeType = navigationResponse.response.mimeType
if let url = navigationResponse.response.url { if let url = navigationResponse.response.url {
if mimeType != nil && !mimeType!.starts(with: "text/") { if mimeType != nil && !mimeType!.starts(with: "text/") {
...@@ -605,6 +606,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi ...@@ -605,6 +606,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
return return
} }
} }
}
decisionHandler(.allow) decisionHandler(.allow)
} }
...@@ -703,27 +705,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi ...@@ -703,27 +705,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
} }
} }
public func onLoadResource(response: URLResponse, fromRequest request: URLRequest?, withData data: Data, startTime: Int64, duration: Int64) { public func onLoadResource(initiatorType: String, url: String, startTime: Double, duration: Double) {
var headersResponse = (response as! HTTPURLResponse).allHeaderFields as! [String: String]
headersResponse.lowercaseKeys()
var headersRequest = request!.allHTTPHeaderFields! as [String: String]
headersRequest.lowercaseKeys()
var arguments: [String : Any] = [ var arguments: [String : Any] = [
"response": [ "initiatorType": initiatorType,
"url": response.url!.absoluteString, "url": url,
"statusCode": (response as! HTTPURLResponse).statusCode,
"headers": headersResponse,
"startTime": startTime, "startTime": startTime,
"duration": duration, "duration": duration
"data": data
],
"request": [
"url": request!.url!.absoluteString,
"headers": headersRequest,
"method": request!.httpMethod!
]
] ]
if IABController != nil { if IABController != nil {
arguments["uuid"] = IABController!.uuid arguments["uuid"] = IABController!.uuid
...@@ -852,24 +839,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi ...@@ -852,24 +839,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
if !UIApplication.shared.canOpenURL(url) { if !UIApplication.shared.canOpenURL(url) {
return return
} }
let startTime: Int64 = Int64(resource["startTime"] as! Double) let initiatorType = resource["initiatorType"] as! String
let duration: Int64 = Int64(resource["duration"] as! Double) let startTime = resource["startTime"] as! Double
var urlRequest = URLRequest(url: url) let duration = resource["duration"] as! Double
urlRequest.allHTTPHeaderFields = [:]
let config = URLSessionConfiguration.default self.onLoadResource(initiatorType: initiatorType, url: url.absoluteString, startTime: startTime, duration: duration)
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) { (data, response, error) in
if error != nil {
print(error)
return
}
var withData = data
if withData == nil {
withData = Data()
}
self.onLoadResource(response: response!, fromRequest: urlRequest, withData: withData!, startTime: startTime, duration: duration)
}
task.resume()
} }
} }
else if message.name == "callHandler" { else if message.name == "callHandler" {
......
...@@ -12,6 +12,7 @@ public class InAppWebViewOptions: Options { ...@@ -12,6 +12,7 @@ public class InAppWebViewOptions: Options {
var useShouldOverrideUrlLoading = false var useShouldOverrideUrlLoading = false
var useOnLoadResource = false var useOnLoadResource = false
var useOnDownloadStart = false
var useOnTargetBlank = false var useOnTargetBlank = false
var clearCache = false var clearCache = false
var userAgent = "" var userAgent = ""
...@@ -21,6 +22,7 @@ public class InAppWebViewOptions: Options { ...@@ -21,6 +22,7 @@ public class InAppWebViewOptions: Options {
var verticalScrollBarEnabled = true var verticalScrollBarEnabled = true
var horizontalScrollBarEnabled = true var horizontalScrollBarEnabled = true
var resourceCustomSchemes: [String] = [] var resourceCustomSchemes: [String] = []
var contentBlockers: [[String: [String : Any]]] = []
var disallowOverScroll = false var disallowOverScroll = false
var enableViewportScale = false var enableViewportScale = false
......
...@@ -28,4 +28,5 @@ export 'src/channel_manager.dart'; ...@@ -28,4 +28,5 @@ export 'src/channel_manager.dart';
export 'src/chrome_safari_browser.dart'; export 'src/chrome_safari_browser.dart';
export 'src/chrome_safari_browser.dart'; export 'src/chrome_safari_browser.dart';
export 'src/in_app_localhost_server.dart'; export 'src/in_app_localhost_server.dart';
export 'src/web_history.dart'; export 'src/webview_options.dart';
export 'src/content_blocker.dart';
class ContentBlocker {
ContentBlockerTrigger trigger;
ContentBlockerAction action;
ContentBlocker(this.trigger, this.action);
Map<String, Map<String, dynamic>> toMap() {
return {
"trigger": trigger.toMap(),
"action": action.toMap()
};
}
}
class ContentBlockerTriggerResourceType {
final String _value;
const ContentBlockerTriggerResourceType._internal(this._value);
toString() => _value;
static const DOCUMENT = const ContentBlockerTriggerResourceType._internal('document');
static const IMAGE = const ContentBlockerTriggerResourceType._internal('image');
static const STYLE_SHEET = const ContentBlockerTriggerResourceType._internal('style-sheet');
static const SCRIPT = const ContentBlockerTriggerResourceType._internal('script');
static const FONT = const ContentBlockerTriggerResourceType._internal('font');
static const MEDIA = const ContentBlockerTriggerResourceType._internal('media');
static const SVG_DOCUMENT = const ContentBlockerTriggerResourceType._internal('svg-document');
static const RAW = const ContentBlockerTriggerResourceType._internal('raw');
}
class ContentBlockerTrigger {
String urlFilter;
List<ContentBlockerTriggerResourceType> resourceType;
ContentBlockerTrigger(this.urlFilter, {this.resourceType = const []});
Map<String, dynamic> toMap() {
List<String> resourceTypeStringList = [];
resourceType.forEach((type) {
resourceTypeStringList.add(type.toString());
});
return {
"url-filter": urlFilter,
"resource-type": resourceTypeStringList
};
}
}
class ContentBlockerActionType {
final String _value;
const ContentBlockerActionType._internal(this._value);
toString() => _value;
static const BLOCK = const ContentBlockerActionType._internal('block');
static const CSS_DISPLAY_NONE = const ContentBlockerActionType._internal('css-display-none');
static const MAKE_HTTPS = const ContentBlockerActionType._internal('make-https');
}
class ContentBlockerAction {
ContentBlockerActionType type;
String selector;
ContentBlockerAction(ContentBlockerActionType type, {String selector}) {
this.type = type;
if (this.type == ContentBlockerActionType.CSS_DISPLAY_NONE) {
assert(selector != null);
}
this.selector = selector;
}
Map<String, dynamic> toMap() {
return {
"type": type.toString(),
"selector": selector
};
}
}
\ No newline at end of file
...@@ -52,6 +52,7 @@ class InAppBrowser { ...@@ -52,6 +52,7 @@ class InAppBrowser {
/// - All platforms support: /// - All platforms support:
/// - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the [shouldOverrideUrlLoading()] event. The default value is `false`. /// - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the [shouldOverrideUrlLoading()] event. The default value is `false`.
/// - __useOnLoadResource__: Set to `true` to be able to listen at the [onLoadResource()] event. The default value is `false`. /// - __useOnLoadResource__: Set to `true` to be able to listen at the [onLoadResource()] event. The default value is `false`.
/// - __useOnDownloadStart__: Set to `true` to be able to listen at the [onDownloadStart()] event. The default value is `false`.
/// - __useOnTargetBlank__: Set to `true` to be able to listen at the [onTargetBlank()] event. The default value is `false`. /// - __useOnTargetBlank__: Set to `true` to be able to listen at the [onTargetBlank()] event. The default value is `false`.
/// - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`. /// - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`.
/// - __userAgent__: Set the custom WebView's user-agent. /// - __userAgent__: Set the custom WebView's user-agent.
...@@ -296,9 +297,7 @@ class InAppBrowser { ...@@ -296,9 +297,7 @@ class InAppBrowser {
///Event fires when the [InAppBrowser] webview loads a resource. ///Event fires when the [InAppBrowser] webview loads a resource.
/// ///
///**NOTE**: In order to be able to listen this event, you need to set `useOnLoadResource` option to `true`. ///**NOTE**: In order to be able to listen this event, you need to set `useOnLoadResource` option to `true`.
/// void onLoadResource(WebResourceResponse response) {
///**NOTE only for iOS**: In some cases, the [response.data] of a [response] with `text/assets` encoding could be empty.
void onLoadResource(WebResourceResponse response, WebResourceRequest request) {
} }
......
...@@ -12,8 +12,8 @@ import 'package:flutter/gestures.dart'; ...@@ -12,8 +12,8 @@ import 'package:flutter/gestures.dart';
import 'types.dart'; import 'types.dart';
import 'in_app_browser.dart'; import 'in_app_browser.dart';
import 'web_history.dart';
import 'channel_manager.dart'; import 'channel_manager.dart';
import 'webview_options.dart';
///Initial [data] as a content for an [InAppWebView] instance, using [baseUrl] as the base URL for it. ///Initial [data] as a content for an [InAppWebView] instance, using [baseUrl] as the base URL for it.
...@@ -44,6 +44,7 @@ class InAppWebViewInitialData { ...@@ -44,6 +44,7 @@ class InAppWebViewInitialData {
///All platforms support these options: ///All platforms support these options:
/// - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the [InAppWebView.shouldOverrideUrlLoading()] event. The default value is `false`. /// - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the [InAppWebView.shouldOverrideUrlLoading()] event. The default value is `false`.
/// - __useOnLoadResource__: Set to `true` to be able to listen at the [InAppWebView.onLoadResource()] event. The default value is `false`. /// - __useOnLoadResource__: Set to `true` to be able to listen at the [InAppWebView.onLoadResource()] event. The default value is `false`.
/// - __useOnDownloadStart__: Set to `true` to be able to listen at the [InAppWebView.onDownloadStart()] event. The default value is `false`.
/// - __useOnTargetBlank__: Set to `true` to be able to listen at the [InAppWebView.onTargetBlank()] event. The default value is `false`. /// - __useOnTargetBlank__: Set to `true` to be able to listen at the [InAppWebView.onTargetBlank()] event. The default value is `false`.
/// - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`. /// - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`.
/// - __userAgent___: Set the custom WebView's user-agent. /// - __userAgent___: Set the custom WebView's user-agent.
...@@ -136,7 +137,7 @@ class InAppWebView extends StatefulWidget { ...@@ -136,7 +137,7 @@ class InAppWebView extends StatefulWidget {
///Initial headers that will be used. ///Initial headers that will be used.
final Map<String, String> initialHeaders; final Map<String, String> initialHeaders;
///Initial options that will be used. ///Initial options that will be used.
final Map<String, dynamic> initialOptions; final List<WebViewOptions> initialOptions;
/// `gestureRecognizers` specifies which gestures should be consumed by the web view. /// `gestureRecognizers` specifies which gestures should be consumed by the web view.
/// It is possible for other gesture recognizers to be competing with the web view on pointer /// It is possible for other gesture recognizers to be competing with the web view on pointer
/// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle
...@@ -152,7 +153,7 @@ class InAppWebView extends StatefulWidget { ...@@ -152,7 +153,7 @@ class InAppWebView extends StatefulWidget {
this.initialFile, this.initialFile,
this.initialData, this.initialData,
this.initialHeaders = const {}, this.initialHeaders = const {},
this.initialOptions = const {}, this.initialOptions = const [],
this.onWebViewCreated, this.onWebViewCreated,
this.onLoadStart, this.onLoadStart,
this.onLoadStop, this.onLoadStop,
...@@ -187,6 +188,11 @@ class _InAppWebViewState extends State<InAppWebView> { ...@@ -187,6 +188,11 @@ class _InAppWebViewState extends State<InAppWebView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map<String, dynamic> initialOptions = {};
widget.initialOptions.forEach((webViewOption) {
initialOptions.addAll(webViewOption.toMap());
});
if (defaultTargetPlatform == TargetPlatform.android) { if (defaultTargetPlatform == TargetPlatform.android) {
return GestureDetector( return GestureDetector(
onLongPress: () {}, onLongPress: () {},
...@@ -201,7 +207,7 @@ class _InAppWebViewState extends State<InAppWebView> { ...@@ -201,7 +207,7 @@ class _InAppWebViewState extends State<InAppWebView> {
'initialFile': widget.initialFile, 'initialFile': widget.initialFile,
'initialData': widget.initialData?.toMap(), 'initialData': widget.initialData?.toMap(),
'initialHeaders': widget.initialHeaders, 'initialHeaders': widget.initialHeaders,
'initialOptions': widget.initialOptions 'initialOptions': initialOptions
}, },
creationParamsCodec: const StandardMessageCodec(), creationParamsCodec: const StandardMessageCodec(),
), ),
...@@ -216,7 +222,7 @@ class _InAppWebViewState extends State<InAppWebView> { ...@@ -216,7 +222,7 @@ class _InAppWebViewState extends State<InAppWebView> {
'initialFile': widget.initialFile, 'initialFile': widget.initialFile,
'initialData': widget.initialData?.toMap(), 'initialData': widget.initialData?.toMap(),
'initialHeaders': widget.initialHeaders, 'initialHeaders': widget.initialHeaders,
'initialOptions': widget.initialOptions 'initialOptions': initialOptions
}, },
creationParamsCodec: const StandardMessageCodec(), creationParamsCodec: const StandardMessageCodec(),
); );
...@@ -307,31 +313,17 @@ class InAppWebViewController { ...@@ -307,31 +313,17 @@ class InAppWebViewController {
_inAppBrowser.shouldOverrideUrlLoading(url); _inAppBrowser.shouldOverrideUrlLoading(url);
break; break;
case "onLoadResource": case "onLoadResource":
Map<dynamic, dynamic> rawResponse = call.arguments["response"]; String initiatorType = call.arguments["initiatorType"];
rawResponse = rawResponse.cast<String, dynamic>(); String url = call.arguments["url"];
Map<dynamic, dynamic> rawRequest = call.arguments["request"]; double startTime = call.arguments["startTime"];
rawRequest = rawRequest.cast<String, dynamic>(); double duration = call.arguments["duration"];
String urlResponse = rawResponse["url"]; var response = new WebResourceResponse(initiatorType, url, startTime, duration);
Map<dynamic, dynamic> headersResponse = rawResponse["headers"];
headersResponse = headersResponse.cast<String, String>();
int statusCode = rawResponse["statusCode"];
int startTime = rawResponse["startTime"];
int duration = rawResponse["duration"];
Uint8List data = rawResponse["data"];
String urlRequest = rawRequest["url"];
Map<dynamic, dynamic> headersRequest = rawRequest["headers"];
headersRequest = headersResponse.cast<String, String>();
String method = rawRequest["method"];
var response = new WebResourceResponse(urlResponse, headersResponse, statusCode, startTime, duration, data);
var request = new WebResourceRequest(urlRequest, headersRequest, method);
if (_widget != null && _widget.onLoadResource != null) if (_widget != null && _widget.onLoadResource != null)
_widget.onLoadResource(this, response, request); _widget.onLoadResource(this, response);
else if (_inAppBrowser != null) else if (_inAppBrowser != null)
_inAppBrowser.onLoadResource(response, request); _inAppBrowser.onLoadResource(response);
break; break;
case "onConsoleMessage": case "onConsoleMessage":
String sourceURL = call.arguments["sourceURL"]; String sourceURL = call.arguments["sourceURL"];
......
...@@ -25,41 +25,34 @@ enum ConsoleMessageLevel { ...@@ -25,41 +25,34 @@ enum ConsoleMessageLevel {
DEBUG, ERROR, LOG, TIP, WARNING DEBUG, ERROR, LOG, TIP, WARNING
} }
///Public class representing a resource request of the [InAppBrowser] WebView.
///It is used by the method [InAppBrowser.onLoadResource()].
class WebResourceRequest {
String url;
Map<String, String> headers;
String method;
WebResourceRequest(this.url, this.headers, this.method);
}
///Public class representing a resource response of the [InAppBrowser] WebView. ///Public class representing a resource response of the [InAppBrowser] WebView.
///It is used by the method [InAppBrowser.onLoadResource()]. ///It is used by the method [InAppBrowser.onLoadResource()].
class WebResourceResponse { class WebResourceResponse {
///A string representing the type of resource.
String initiatorType;
///Resource URL.
String url; String url;
Map<String, String> headers; ///Returns the [DOMHighResTimeStamp](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) for the time a resource fetch started.
int statusCode; double startTime;
int startTime; ///Returns the [DOMHighResTimeStamp](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) duration to fetch a resource.
int duration; double duration;
Uint8List data;
WebResourceResponse(this.url, this.headers, this.statusCode, this.startTime, this.duration, this.data); WebResourceResponse(this.initiatorType, this.url, this.startTime, this.duration);
} }
///Public class representing the response returned by the [onLoadResourceCustomScheme()] event of [InAppWebView]. ///Public class representing the response returned by the [onLoadResourceCustomScheme()] event of [InAppWebView].
///It allows to load a specific resource. The resource data must be encoded to `base64`. ///It allows to load a specific resource. The resource data must be encoded to `base64`.
class CustomSchemeResponse { class CustomSchemeResponse {
///Data enconded to 'base64'.
String base64data; String base64data;
///Content-Type of the data, such as `image/png`.
String contentType; String contentType;
///Content-Enconding of the data, such as `utf-8`.
String contentEnconding; String contentEnconding;
CustomSchemeResponse(this.base64data, this.contentType, this.contentEnconding); CustomSchemeResponse(this.base64data, this.contentType, {this.contentEnconding = 'utf-8'});
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
...@@ -84,6 +77,37 @@ class ConsoleMessage { ...@@ -84,6 +77,37 @@ class ConsoleMessage {
ConsoleMessage(this.sourceURL, this.lineNumber, this.message, this.messageLevel); ConsoleMessage(this.sourceURL, this.lineNumber, this.message, this.messageLevel);
} }
///WebHistory class.
///
///This class contains a snapshot of the current back/forward list for a WebView.
class WebHistory {
List<WebHistoryItem> _list;
///List of all [WebHistoryItem]s.
List<WebHistoryItem> get list => _list;
///Index of the current [WebHistoryItem].
int currentIndex;
WebHistory(this._list, this.currentIndex);
}
///WebHistoryItem class.
///
///A convenience class for accessing fields in an entry in the back/forward list of a WebView. Each WebHistoryItem is a snapshot of the requested history item.
class WebHistoryItem {
///Original url of this history item.
String originalUrl;
///Document title of this history item.
String title;
///Url of this history item.
String url;
///0-based position index in the back-forward [WebHistory.list].
int index;
///Position offset respect to the currentIndex of the back-forward [WebHistory.list].
int offset;
WebHistoryItem(this.originalUrl, this.title, this.url, this.index, this.offset);
}
typedef onWebViewCreatedCallback = void Function(InAppWebViewController controller); typedef onWebViewCreatedCallback = void Function(InAppWebViewController controller);
typedef onWebViewLoadStartCallback = void Function(InAppWebViewController controller, String url); typedef onWebViewLoadStartCallback = void Function(InAppWebViewController controller, String url);
typedef onWebViewLoadStopCallback = void Function(InAppWebViewController controller, String url); typedef onWebViewLoadStopCallback = void Function(InAppWebViewController controller, String url);
...@@ -91,7 +115,7 @@ typedef onWebViewLoadErrorCallback = void Function(InAppWebViewController contro ...@@ -91,7 +115,7 @@ typedef onWebViewLoadErrorCallback = void Function(InAppWebViewController contro
typedef onWebViewProgressChangedCallback = void Function(InAppWebViewController controller, int progress); typedef onWebViewProgressChangedCallback = void Function(InAppWebViewController controller, int progress);
typedef onWebViewConsoleMessageCallback = void Function(InAppWebViewController controller, ConsoleMessage consoleMessage); typedef onWebViewConsoleMessageCallback = void Function(InAppWebViewController controller, ConsoleMessage consoleMessage);
typedef shouldOverrideUrlLoadingCallback = void Function(InAppWebViewController controller, String url); typedef shouldOverrideUrlLoadingCallback = void Function(InAppWebViewController controller, String url);
typedef onWebViewLoadResourceCallback = void Function(InAppWebViewController controller, WebResourceResponse response, WebResourceRequest request); typedef onWebViewLoadResourceCallback = void Function(InAppWebViewController controller, WebResourceResponse response);
typedef onWebViewScrollChangedCallback = void Function(InAppWebViewController controller, int x, int y); typedef onWebViewScrollChangedCallback = void Function(InAppWebViewController controller, int x, int y);
typedef onDownloadStartCallback = void Function(InAppWebViewController controller, String url); typedef onDownloadStartCallback = void Function(InAppWebViewController controller, String url);
typedef onLoadResourceCustomSchemeCallback = Future<CustomSchemeResponse> Function(InAppWebViewController controller, String scheme, String url); typedef onLoadResourceCustomSchemeCallback = Future<CustomSchemeResponse> Function(InAppWebViewController controller, String scheme, String url);
......
///WebHistory class.
///
///This class contains a snapshot of the current back/forward list for a WebView.
class WebHistory {
List<WebHistoryItem> _list;
///List of all [WebHistoryItem]s.
List<WebHistoryItem> get list => _list;
///Index of the current [WebHistoryItem].
int currentIndex;
WebHistory(this._list, this.currentIndex);
}
///WebHistoryItem class.
///
///A convenience class for accessing fields in an entry in the back/forward list of a WebView. Each WebHistoryItem is a snapshot of the requested history item.
class WebHistoryItem {
///Original url of this history item.
String originalUrl;
///Document title of this history item.
String title;
///Url of this history item.
String url;
///0-based position index in the back-forward [WebHistory.list].
int index;
///Position offset respect to the currentIndex of the back-forward [WebHistory.list].
int offset;
WebHistoryItem(this.originalUrl, this.title, this.url, this.index, this.offset);
}
\ No newline at end of file
import 'package:flutter_inappbrowser/src/content_blocker.dart';
class WebViewOptions {
Map<String, dynamic> toMap() {
return {};
}
}
class InAppWebViewOptions implements WebViewOptions {
bool useShouldOverrideUrlLoading;
bool useOnLoadResource;
bool useOnDownloadStart;
bool useOnTargetBlank;
bool clearCache;
String userAgent;
bool javaScriptEnabled;
bool javaScriptCanOpenWindowsAutomatically;
bool mediaPlaybackRequiresUserGesture;
int textZoom;
bool verticalScrollBarEnabled;
bool horizontalScrollBarEnabled;
List<String> resourceCustomSchemes;
List<ContentBlocker> contentBlockers;
InAppWebViewOptions({this.useShouldOverrideUrlLoading = false, this.useOnLoadResource = false, this.useOnDownloadStart = false, this.useOnTargetBlank = false,
this.clearCache = false, this.userAgent = "", this.javaScriptEnabled = true, this.javaScriptCanOpenWindowsAutomatically = false,
this.mediaPlaybackRequiresUserGesture = true, this.textZoom = 100, this.verticalScrollBarEnabled = true, this.horizontalScrollBarEnabled = true, this.resourceCustomSchemes = const [],
this.contentBlockers = const []});
@override
Map<String, dynamic> toMap() {
List<Map<String, Map<String, dynamic>>> contentBlockersMapList = [];
contentBlockers.forEach((contentBlocker) {
contentBlockersMapList.add(contentBlocker.toMap());
});
return {
"useShouldOverrideUrlLoading": useShouldOverrideUrlLoading,
"useOnLoadResource": useOnLoadResource,
"useOnDownloadStart": useOnDownloadStart,
"useOnTargetBlank": useOnTargetBlank,
"clearCache": clearCache,
"userAgent": userAgent,
"javaScriptEnabled": javaScriptEnabled,
"javaScriptCanOpenWindowsAutomatically": javaScriptCanOpenWindowsAutomatically,
"mediaPlaybackRequiresUserGesture": mediaPlaybackRequiresUserGesture,
"textZoom": textZoom,
"verticalScrollBarEnabled": verticalScrollBarEnabled,
"horizontalScrollBarEnabled": horizontalScrollBarEnabled,
"resourceCustomSchemes": resourceCustomSchemes,
"contentBlockers": contentBlockersMapList,
};
}
}
class AndroidInAppWebViewOptions implements WebViewOptions {
bool clearSessionCache;
bool builtInZoomControls;
bool displayZoomControls;
bool supportZoom;
bool databaseEnabled;
bool domStorageEnabled;
bool useWideViewPort;
bool safeBrowsingEnabled;
bool transparentBackground;
String mixedContentMode;
AndroidInAppWebViewOptions({this.clearSessionCache = false, this.builtInZoomControls = false, this.displayZoomControls = false, this.supportZoom = true, this.databaseEnabled = false,
this.domStorageEnabled = false, this.useWideViewPort = true, this.safeBrowsingEnabled = true, this.transparentBackground = false, this.mixedContentMode = ""});
@override
Map<String, dynamic> toMap() {
return {
"clearSessionCache": clearSessionCache,
"builtInZoomControls": builtInZoomControls,
"displayZoomControls": displayZoomControls,
"supportZoom": supportZoom,
"databaseEnabled": databaseEnabled,
"domStorageEnabled": domStorageEnabled,
"useWideViewPort": useWideViewPort,
"safeBrowsingEnabled": safeBrowsingEnabled,
"transparentBackground": transparentBackground,
"mixedContentMode": mixedContentMode,
};
}
}
class iOSInAppWebViewOptions implements WebViewOptions {
bool disallowOverScroll;
bool enableViewportScale;
bool suppressesIncrementalRendering;
bool allowsAirPlayForMediaPlayback;
bool allowsBackForwardNavigationGestures;
bool allowsLinkPreview;
bool ignoresViewportScaleLimits;
bool allowsInlineMediaPlayback;
bool allowsPictureInPictureMediaPlayback;
bool transparentBackground;
iOSInAppWebViewOptions({this.disallowOverScroll = false, this.enableViewportScale = false, this.suppressesIncrementalRendering = false, this.allowsAirPlayForMediaPlayback = true,
this.allowsBackForwardNavigationGestures = true, this.allowsLinkPreview = true, this.ignoresViewportScaleLimits = false, this.allowsInlineMediaPlayback = false,
this.allowsPictureInPictureMediaPlayback = true, this.transparentBackground = false});
@override
Map<String, dynamic> toMap() {
return {
"disallowOverScroll": disallowOverScroll,
"enableViewportScale": enableViewportScale,
"suppressesIncrementalRendering": suppressesIncrementalRendering,
"allowsAirPlayForMediaPlayback": allowsAirPlayForMediaPlayback,
"allowsBackForwardNavigationGestures": allowsBackForwardNavigationGestures,
"allowsLinkPreview": allowsLinkPreview,
"ignoresViewportScaleLimits": ignoresViewportScaleLimits,
"allowsInlineMediaPlayback": allowsInlineMediaPlayback,
"allowsPictureInPictureMediaPlayback": allowsPictureInPictureMediaPlayback,
"transparentBackground": transparentBackground,
};
}
}
class InAppBrowserOptions implements WebViewOptions {
bool hidden;
bool toolbarTop;
String toolbarTopBackgroundColor;
String toolbarTopFixedTitle;
bool hideUrlBar;
InAppBrowserOptions({this.hidden = false, this.toolbarTop = true, this.toolbarTopBackgroundColor = "", this.toolbarTopFixedTitle = "", this.hideUrlBar = false});
@override
Map<String, dynamic> toMap() {
return {
"hidden": hidden,
"toolbarTop": toolbarTop,
"toolbarTopBackgroundColor": toolbarTopBackgroundColor,
"toolbarTopFixedTitle": toolbarTopFixedTitle,
"hideUrlBar": hideUrlBar,
};
}
}
class AndroidInAppBrowserOptions implements WebViewOptions {
bool hideTitleBar;
bool closeOnCannotGoBack;
bool progressBar;
AndroidInAppBrowserOptions({this.hideTitleBar = true, this.closeOnCannotGoBack = true, this.progressBar = true});
@override
Map<String, dynamic> toMap() {
return {
"hideTitleBar": hideTitleBar,
"closeOnCannotGoBack": closeOnCannotGoBack,
"progressBar": progressBar,
};
}
}
class iOSInAppBrowserOptions implements WebViewOptions {
bool toolbarBottom;
String toolbarBottomBackgroundColor;
bool toolbarBottomTranslucent;
String closeButtonCaption;
String closeButtonColor;
int presentationStyle; //default fullscreen
int transitionStyle; //default crossDissolve
bool spinner;
iOSInAppBrowserOptions({this.toolbarBottom = true, this.toolbarBottomBackgroundColor = "", this.toolbarBottomTranslucent = true, this.closeButtonCaption = "",
this.closeButtonColor = "", this.presentationStyle = 0, this.transitionStyle = 0, this.spinner = true});
@override
Map<String, dynamic> toMap() {
return {
"toolbarBottom": toolbarBottom,
"toolbarBottomBackgroundColor": toolbarBottomBackgroundColor,
"toolbarBottomTranslucent": toolbarBottomTranslucent,
"closeButtonCaption": closeButtonCaption,
"closeButtonColor": closeButtonColor,
"presentationStyle": presentationStyle,
"transitionStyle": transitionStyle,
"spinner": spinner,
};
}
}
class AndroidChromeCustomTabsOptions implements WebViewOptions {
bool addShareButton;
bool showTitle;
String toolbarBackgroundColor;
bool enableUrlBarHiding;
bool instantAppsEnabled;
AndroidChromeCustomTabsOptions({this.addShareButton = true, this.showTitle = true, this.toolbarBackgroundColor = "", this.enableUrlBarHiding = false, this.instantAppsEnabled = false});
@override
Map<String, dynamic> toMap() {
return {
"addShareButton": addShareButton,
"showTitle": showTitle,
"toolbarBackgroundColor": toolbarBackgroundColor,
"enableUrlBarHiding": enableUrlBarHiding,
"instantAppsEnabled": instantAppsEnabled,
};
}
}
class iOSChromeCustomTabsOptions implements WebViewOptions {
bool entersReaderIfAvailable;
bool barCollapsingEnabled;
int dismissButtonStyle; //default done
String preferredBarTintColor;
String preferredControlTintColor;
int presentationStyle; //default fullscreen
int transitionStyle; //default crossDissolve
iOSChromeCustomTabsOptions({this.entersReaderIfAvailable = false, this.barCollapsingEnabled = false, this.dismissButtonStyle = 0, this.preferredBarTintColor = "",
this.preferredControlTintColor = "", this.presentationStyle = 0, this.transitionStyle = 0});
@override
Map<String, dynamic> toMap() {
return {
"entersReaderIfAvailable": entersReaderIfAvailable,
"barCollapsingEnabled": barCollapsingEnabled,
"dismissButtonStyle": dismissButtonStyle,
"preferredBarTintColor": preferredBarTintColor,
"preferredControlTintColor": preferredControlTintColor,
"presentationStyle": presentationStyle,
"transitionStyle": transitionStyle,
};
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment