import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_webview_plugin/src/javascript_channel.dart'; import 'javascript_message.dart'; const _kChannel = 'flutter_webview_plugin'; // TODO: more general state for iOS/android enum WebViewState { shouldStart, startLoad, finishLoad, abortLoad } // TODO: use an id by webview to be able to manage multiple webview /// Singleton class that communicate with a Webview Instance class FlutterWebviewPlugin { factory FlutterWebviewPlugin() { if(_instance == null) { const MethodChannel methodChannel = const MethodChannel(_kChannel); _instance = FlutterWebviewPlugin.private(methodChannel); } return _instance; } @visibleForTesting FlutterWebviewPlugin.private(this._channel) { _channel.setMethodCallHandler(_handleMessages); } static FlutterWebviewPlugin _instance; final MethodChannel _channel; final _onBack = StreamController.broadcast(); final _onDestroy = StreamController.broadcast(); final _onUrlChanged = StreamController.broadcast(); final _onStateChanged = StreamController.broadcast(); final _onScrollXChanged = StreamController.broadcast(); final _onScrollYChanged = StreamController.broadcast(); final _onProgressChanged = new StreamController.broadcast(); final _onHttpError = StreamController.broadcast(); final _onPostMessage = StreamController.broadcast(); final Map _javascriptChannels = // ignoring warning as min SDK version doesn't support collection literals yet // ignore: prefer_collection_literals Map(); Future _handleMessages(MethodCall call) async { switch (call.method) { case 'onBack': _onBack.add(null); break; case 'onDestroy': _onDestroy.add(null); break; case 'onUrlChanged': _onUrlChanged.add(call.arguments['url']); break; case 'onScrollXChanged': _onScrollXChanged.add(call.arguments['xDirection']); break; case 'onScrollYChanged': _onScrollYChanged.add(call.arguments['yDirection']); break; case 'onProgressChanged': _onProgressChanged.add(call.arguments['progress']); break; case 'onState': _onStateChanged.add( WebViewStateChanged.fromMap( Map.from(call.arguments), ), ); break; case 'onHttpError': _onHttpError.add( WebViewHttpError(call.arguments['code'], call.arguments['url'])); break; case 'javascriptChannelMessage': _handleJavascriptChannelMessage( call.arguments['channel'], call.arguments['message']); break; } } /// Listening the OnDestroy LifeCycle Event for Android Stream get onDestroy => _onDestroy.stream; /// Listening the back key press Event for Android Stream get onBack => _onBack.stream; /// Listening url changed Stream get onUrlChanged => _onUrlChanged.stream; /// Listening the onState Event for iOS WebView and Android /// content is Map for type: {shouldStart(iOS)|startLoad|finishLoad} /// more detail than other events Stream get onStateChanged => _onStateChanged.stream; /// Listening web view loading progress estimation, value between 0.0 and 1.0 Stream get onProgressChanged => _onProgressChanged.stream; /// Listening web view y position scroll change Stream get onScrollYChanged => _onScrollYChanged.stream; /// Listening web view x position scroll change Stream get onScrollXChanged => _onScrollXChanged.stream; Stream get onHttpError => _onHttpError.stream; /// Start the Webview with [url] /// - [headers] specify additional HTTP headers /// - [withJavascript] enable Javascript or not for the Webview /// - [clearCache] clear the cache of the Webview /// - [clearCookies] clear all cookies of the Webview /// - [hidden] not show /// - [rect]: show in rect, fullscreen if null /// - [enableAppScheme]: false will enable all schemes, true only for httt/https/about /// android: Not implemented yet /// - [userAgent]: set the User-Agent of WebView /// - [withZoom]: enable zoom on webview /// - [withLocalStorage] enable localStorage API on Webview /// Currently Android only. /// It is always enabled in UIWebView of iOS and can not be disabled. /// - [withLocalUrl]: allow url as a local path /// Allow local files on iOs > 9.0 /// - [localUrlScope]: allowed folder for local paths /// iOS only. /// If null and withLocalUrl is true, then it will use the url as the scope, /// allowing only itself to be read. /// - [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. /// Android only settings: /// - [displayZoomControls]: display zoom controls on webview /// - [withOverviewMode]: enable overview mode for Android webview ( setLoadWithOverviewMode ) /// - [useWideViewPort]: use wide viewport for Android webview ( setUseWideViewPort ) Future launch( String url, { Map headers, Set javascriptChannels, bool withJavascript, bool clearCache, bool clearCookies, bool hidden, bool enableAppScheme, Rect rect, String userAgent, bool withZoom, bool displayZoomControls, bool withLocalStorage, bool withLocalUrl, String localUrlScope, bool withOverviewMode, bool scrollBar, bool supportMultipleWindows, bool appCacheEnabled, bool allowFileURLs, bool useWideViewPort, String invalidUrlRegex, bool geolocationEnabled, bool debuggingEnabled, }) async { final args = { 'url': url, 'withJavascript': withJavascript ?? true, 'clearCache': clearCache ?? false, 'hidden': hidden ?? false, 'clearCookies': clearCookies ?? false, 'enableAppScheme': enableAppScheme ?? true, 'userAgent': userAgent, 'withZoom': withZoom ?? false, 'displayZoomControls': displayZoomControls ?? false, 'withLocalStorage': withLocalStorage ?? true, 'withLocalUrl': withLocalUrl ?? false, 'localUrlScope': localUrlScope, 'scrollBar': scrollBar ?? true, 'supportMultipleWindows': supportMultipleWindows ?? false, 'appCacheEnabled': appCacheEnabled ?? false, 'allowFileURLs': allowFileURLs ?? false, 'useWideViewPort': useWideViewPort ?? false, 'invalidUrlRegex': invalidUrlRegex, 'geolocationEnabled': geolocationEnabled ?? false, 'withOverviewMode': withOverviewMode ?? false, 'debuggingEnabled': debuggingEnabled ?? false, }; if (headers != null) { args['headers'] = headers; } _assertJavascriptChannelNamesAreUnique(javascriptChannels); if (javascriptChannels != null) { javascriptChannels.forEach((channel) { _javascriptChannels[channel.name] = channel; }); } else { if (_javascriptChannels.isNotEmpty) { _javascriptChannels.clear(); } } args['javascriptChannelNames'] = _extractJavascriptChannelNames(javascriptChannels).toList(); if (rect != null) { args['rect'] = { 'left': rect.left, 'top': rect.top, 'width': rect.width, 'height': rect.height, }; } await _channel.invokeMethod('launch', args); } /// Execute Javascript inside webview Future evalJavascript(String code) async { final res = await _channel.invokeMethod('eval', {'code': code}); return res; } /// Close the Webview /// Will trigger the [onDestroy] event Future close() async { _javascriptChannels.clear(); await _channel.invokeMethod('close'); } /// Reloads the WebView. Future reload() async => await _channel.invokeMethod('reload'); /// Navigates back on the Webview. Future goBack() async => await _channel.invokeMethod('back'); /// Checks if webview can navigate back Future canGoBack() async => await _channel.invokeMethod('canGoBack'); /// Checks if webview can navigate back Future canGoForward() async => await _channel.invokeMethod('canGoForward'); /// Navigates forward on the Webview. Future goForward() async => await _channel.invokeMethod('forward'); // Hides the webview Future hide() async => await _channel.invokeMethod('hide'); // Shows the webview Future show() async => await _channel.invokeMethod('show'); // Reload webview with a url Future reloadUrl(String url, {Map headers}) async { final args = {'url': url}; if (headers != null) { args['headers'] = headers; } await _channel.invokeMethod('reloadUrl', args); } // Clean cookies on WebView Future cleanCookies() async { // one liner to clear javascript cookies await evalJavascript('document.cookie.split(";").forEach(function(c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); });'); return await _channel.invokeMethod('cleanCookies'); } // Stops current loading process Future stopLoading() async => await _channel.invokeMethod('stopLoading'); /// Close all Streams void dispose() { _onDestroy.close(); _onUrlChanged.close(); _onStateChanged.close(); _onProgressChanged.close(); _onScrollXChanged.close(); _onScrollYChanged.close(); _onHttpError.close(); _onPostMessage.close(); _instance = null; } Future> getCookies() async { final cookiesString = await evalJavascript('document.cookie'); final cookies = {}; if (cookiesString?.isNotEmpty == true) { cookiesString.split(';').forEach((String cookie) { final split = cookie.split('='); cookies[split[0]] = split[1]; }); } return cookies; } /// resize webview Future resize(Rect rect) async { final args = {}; args['rect'] = { 'left': rect.left, 'top': rect.top, 'width': rect.width, 'height': rect.height, }; await _channel.invokeMethod('resize', args); } Set _extractJavascriptChannelNames(Set channels) { final Set channelNames = channels == null // ignore: prefer_collection_literals ? Set() : channels.map((JavascriptChannel channel) => channel.name).toSet(); return channelNames; } void _handleJavascriptChannelMessage( final String channelName, final String message) { _javascriptChannels[channelName] .onMessageReceived(JavascriptMessage(message)); } void _assertJavascriptChannelNamesAreUnique( final Set channels) { if (channels == null || channels.isEmpty) { return; } assert(_extractJavascriptChannelNames(channels).length == channels.length); } } class WebViewStateChanged { WebViewStateChanged(this.type, this.url, this.navigationType); factory WebViewStateChanged.fromMap(Map map) { WebViewState t; switch (map['type']) { case 'shouldStart': t = WebViewState.shouldStart; break; case 'startLoad': t = WebViewState.startLoad; break; case 'finishLoad': t = WebViewState.finishLoad; break; case 'abortLoad': t = WebViewState.abortLoad; break; } return WebViewStateChanged(t, map['url'], map['navigationType']); } final WebViewState type; final String url; final int navigationType; } class WebViewHttpError { WebViewHttpError(this.code, this.url); final String url; final String code; }