diff --git a/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java b/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java index 55583883..a052a12b 100644 --- a/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java +++ b/android/src/main/java/com/flutter_webview_plugin/BrowserClient.java @@ -1,6 +1,8 @@ package com.flutter_webview_plugin; +import android.annotation.TargetApi; import android.graphics.Bitmap; +import android.os.Build; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebView; @@ -8,14 +10,33 @@ import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Created by lejard_h on 20/12/2017. */ public class BrowserClient extends WebViewClient { + private Pattern invalidUrlPattern = null; + public BrowserClient() { + this(null); + } + + public BrowserClient(String invalidUrlRegex) { super(); + if (invalidUrlRegex != null) { + invalidUrlPattern = Pattern.compile(invalidUrlRegex); + } + } + + public void updateInvalidUrlRegex(String invalidUrlRegex) { + if (invalidUrlRegex != null) { + invalidUrlPattern = Pattern.compile(invalidUrlRegex); + } else { + invalidUrlPattern = null; + } } @Override @@ -40,6 +61,35 @@ public void onPageFinished(WebView view, String url) { } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + // returning true causes the current WebView to abort loading the URL, + // while returning false causes the WebView to continue loading the URL as usual. + String url = request.getUrl().toString(); + boolean isInvalid = checkInvalidUrl(url); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", isInvalid ? "abortLoad" : "shouldStart"); + + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + return isInvalid; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // returning true causes the current WebView to abort loading the URL, + // while returning false causes the WebView to continue loading the URL as usual. + boolean isInvalid = checkInvalidUrl(url); + Map data = new HashMap<>(); + data.put("url", url); + data.put("type", isInvalid ? "abortLoad" : "shouldStart"); + + FlutterWebviewPlugin.channel.invokeMethod("onState", data); + return isInvalid; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { super.onReceivedHttpError(view, request, errorResponse); @@ -48,4 +98,22 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes data.put("code", Integer.toString(errorResponse.getStatusCode())); FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + Map data = new HashMap<>(); + data.put("url", failingUrl); + data.put("code", errorCode); + FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); + } + + private boolean checkInvalidUrl(String url) { + if (invalidUrlPattern == null) { + return false; + } else { + Matcher matcher = invalidUrlPattern.matcher(url); + return matcher.lookingAt(); + } + } } \ No newline at end of file diff --git a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java index a9433516..65b7e947 100644 --- a/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java +++ b/android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java @@ -97,6 +97,8 @@ private void openUrl(MethodCall call, MethodChannel.Result result) { Map headers = call.argument("headers"); boolean scrollBar = call.argument("scrollBar"); boolean allowFileURLs = call.argument("allowFileURLs"); + boolean useWideViewPort = call.argument("useWideViewPort"); + String invalidUrlRegex = call.argument("invalidUrlRegex"); boolean geolocationEnabled = call.argument("geolocationEnabled"); if (webViewManager == null || webViewManager.closed == true) { @@ -120,6 +122,8 @@ private void openUrl(MethodCall call, MethodChannel.Result result) { supportMultipleWindows, appCacheEnabled, allowFileURLs, + useWideViewPort, + invalidUrlRegex, geolocationEnabled ); result.success(null); diff --git a/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java b/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java index 756b6027..1d167e6f 100644 --- a/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java +++ b/android/src/main/java/com/flutter_webview_plugin/WebviewManager.java @@ -2,7 +2,6 @@ import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.annotation.TargetApi; import android.app.Activity; import android.os.Build; @@ -15,7 +14,6 @@ import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.FrameLayout; import java.util.HashMap; @@ -75,13 +73,14 @@ public boolean handleResult(int requestCode, int resultCode, Intent intent){ boolean closed = false; WebView webView; Activity activity; + BrowserClient webViewClient; ResultHandler resultHandler; WebviewManager(final Activity activity) { this.webView = new ObservableWebView(activity); this.activity = activity; this.resultHandler = new ResultHandler(); - WebViewClient webViewClient = new BrowserClient(); + webViewClient = new BrowserClient(); webView.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { @@ -206,6 +205,8 @@ void openUrl( boolean supportMultipleWindows, boolean appCacheEnabled, boolean allowFileURLs, + boolean useWideViewPort, + String invalidUrlRegex, boolean geolocationEnabled ) { webView.getSettings().setJavaScriptEnabled(withJavascript); @@ -221,6 +222,10 @@ void openUrl( webView.getSettings().setAllowFileAccessFromFileURLs(allowFileURLs); webView.getSettings().setAllowUniversalAccessFromFileURLs(allowFileURLs); + webView.getSettings().setUseWideViewPort(useWideViewPort); + + webViewClient.updateInvalidUrlRegex(invalidUrlRegex); + if (geolocationEnabled) { webView.getSettings().setGeolocationEnabled(true); webView.setWebChromeClient(new WebChromeClient() { diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 8d163b0d..3a963017 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -25,6 +25,7 @@ android { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.yourcompany.flutter_webview_plugin_example" + minSdkVersion 16 } buildTypes { diff --git a/example/lib/main.dart b/example/lib/main.dart index 91cc2dbd..f3ea78d3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -201,6 +201,7 @@ class _MyHomePageState extends State { selectedUrl, rect: Rect.fromLTWH(0.0, 0.0, MediaQuery.of(context).size.width, 300.0), userAgent: kAndroidUserAgent, + invalidUrlRegex: r'^(https).+(twitter)', // prevent redirecting to twitter when user click on its icon in flutter website ); }, child: const Text('Open Webview (rect)'), diff --git a/ios/Classes/FlutterWebviewPlugin.m b/ios/Classes/FlutterWebviewPlugin.m index 56470f9c..6cbc7769 100644 --- a/ios/Classes/FlutterWebviewPlugin.m +++ b/ios/Classes/FlutterWebviewPlugin.m @@ -6,6 +6,7 @@ @interface FlutterWebviewPlugin() { BOOL _enableAppScheme; BOOL _enableZoom; + NSString* _invalidUrlRegex; } @end @@ -83,6 +84,8 @@ - (void)initWebview:(FlutterMethodCall*)call { NSString *userAgent = call.arguments[@"userAgent"]; NSNumber *withZoom = call.arguments[@"withZoom"]; NSNumber *scrollBar = call.arguments[@"scrollBar"]; + NSNumber *withJavascript = call.arguments[@"withJavascript"]; + _invalidUrlRegex = call.arguments[@"invalidUrlRegex"]; if (clearCache != (id)[NSNull null] && [clearCache boolValue]) { [[NSURLCache sharedURLCache] removeAllCachedResponses]; @@ -112,9 +115,18 @@ - (void)initWebview:(FlutterMethodCall*)call { self.webview.scrollView.showsHorizontalScrollIndicator = [scrollBar boolValue]; self.webview.scrollView.showsVerticalScrollIndicator = [scrollBar boolValue]; + WKPreferences* preferences = [[self.webview configuration] preferences]; + if ([withJavascript boolValue]) { + [preferences setJavaScriptEnabled:YES]; + } else { + [preferences setJavaScriptEnabled:NO]; + } + _enableZoom = [withZoom boolValue]; - [self.viewController.view addSubview:self.webview]; + UIViewController* presentedViewController = self.viewController.presentedViewController; + UIViewController* currentViewController = presentedViewController != nil ? presentedViewController : self.viewController; + [currentViewController.view addSubview:self.webview]; [self navigate:call]; } @@ -235,18 +247,37 @@ - (void)cleanCookies { }]; } +- (bool)checkInvalidUrl:(NSURL*)url { + NSString* urlString = url != nil ? [url absoluteString] : nil; + if (_invalidUrlRegex != [NSNull null] && urlString != nil) { + NSError* error = NULL; + NSRegularExpression* regex = + [NSRegularExpression regularExpressionWithPattern:_invalidUrlRegex + options:NSRegularExpressionCaseInsensitive + error:&error]; + NSTextCheckingResult* match = [regex firstMatchInString:urlString + options:0 + range:NSMakeRange(0, [urlString length])]; + return match != nil; + } else { + return false; + } +} + #pragma mark -- WkWebView Delegate - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + BOOL isInvalid = [self checkInvalidUrl: navigationAction.request.URL]; + id data = @{@"url": navigationAction.request.URL.absoluteString, - @"type": @"shouldStart", + @"type": isInvalid ? @"abortLoad" : @"shouldStart", @"navigationType": [NSNumber numberWithInt:navigationAction.navigationType]}; [channel invokeMethod:@"onState" arguments:data]; if (navigationAction.navigationType == WKNavigationTypeBackForward) { [channel invokeMethod:@"onBackPressed" arguments:nil]; - } else { + } else if (!isInvalid) { id data = @{@"url": navigationAction.request.URL.absoluteString}; [channel invokeMethod:@"onUrlChanged" arguments:data]; } @@ -255,7 +286,11 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati ([webView.URL.scheme isEqualToString:@"http"] || [webView.URL.scheme isEqualToString:@"https"] || [webView.URL.scheme isEqualToString:@"about"])) { - decisionHandler(WKNavigationActionPolicyAllow); + if (isInvalid) { + decisionHandler(WKNavigationActionPolicyCancel); + } else { + decisionHandler(WKNavigationActionPolicyAllow); + } } else { decisionHandler(WKNavigationActionPolicyCancel); } diff --git a/lib/src/base.dart b/lib/src/base.dart index d0b232ab..fe18c7e7 100644 --- a/lib/src/base.dart +++ b/lib/src/base.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; const _kChannel = 'flutter_webview_plugin'; // TODO: more general state for iOS/android -enum WebViewState { shouldStart, startLoad, finishLoad } +enum WebViewState { shouldStart, startLoad, finishLoad, abortLoad } // TODO: use an id by webview to be able to manage multiple webview @@ -79,7 +79,6 @@ class FlutterWebviewPlugin { /// Start the Webview with [url] /// - [headers] specify additional HTTP headers /// - [withJavascript] enable Javascript or not for the Webview - /// iOS WebView: Not implemented yet /// - [clearCache] clear the cache of the Webview /// - [clearCookies] clear all cookies of the Webview /// - [hidden] not show @@ -94,6 +93,10 @@ class FlutterWebviewPlugin { /// - [withLocalUrl]: allow url as a local path /// Allow local files on iOs > 9.0 /// - [scrollBar]: enable or disable scrollbar + /// - [supportMultipleWindows] enable multiple windows support in Android + /// - [invalidUrlRegex] is the regular expression of URLs that web view shouldn't load. + /// For example, when webview is redirected to a specific URL, you want to intercept + /// this process by stopping loading this URL and replacing webview by another screen. Future launch(String url, { Map headers, bool withJavascript, @@ -110,6 +113,8 @@ class FlutterWebviewPlugin { bool supportMultipleWindows, bool appCacheEnabled, bool allowFileURLs, + bool useWideViewPort, + String invalidUrlRegex, bool geolocationEnabled, }) async { final args = { @@ -127,6 +132,8 @@ class FlutterWebviewPlugin { 'supportMultipleWindows': supportMultipleWindows ?? false, 'appCacheEnabled': appCacheEnabled ?? false, 'allowFileURLs': allowFileURLs ?? false, + 'useWideViewPort': useWideViewPort ?? false, + 'invalidUrlRegex': invalidUrlRegex, 'geolocationEnabled': geolocationEnabled ?? false, }; @@ -235,6 +242,9 @@ class WebViewStateChanged { case 'finishLoad': t = WebViewState.finishLoad; break; + case 'abortLoad': + t = WebViewState.abortLoad; + break; } return WebViewStateChanged(t, map['url'], map['navigationType']); } diff --git a/lib/src/webview_scaffold.dart b/lib/src/webview_scaffold.dart index 0ea447a9..0996ca4e 100644 --- a/lib/src/webview_scaffold.dart +++ b/lib/src/webview_scaffold.dart @@ -31,6 +31,7 @@ class WebviewScaffold extends StatefulWidget { this.initialChild, this.allowFileURLs, this.resizeToAvoidBottomInset = false, + this.invalidUrlRegex, this.geolocationEnabled }) : super(key: key); @@ -55,6 +56,7 @@ class WebviewScaffold extends StatefulWidget { final Widget initialChild; final bool allowFileURLs; final bool resizeToAvoidBottomInset; + final String invalidUrlRegex; final bool geolocationEnabled; @override @@ -129,6 +131,7 @@ class _WebviewScaffoldState extends State { supportMultipleWindows: widget.supportMultipleWindows, appCacheEnabled: widget.appCacheEnabled, allowFileURLs: widget.allowFileURLs, + invalidUrlRegex: widget.invalidUrlRegex, geolocationEnabled: widget.geolocationEnabled ); } else {