You are on page 1of 13

// Copyright 2018 The Chromium Authors. All rights reserved.

// Use of this source code is governed by a BSD-style license that can be


// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

import 'platform_interface.dart';
import 'src/webview_android.dart';
import 'src/webview_cupertino.dart';

/// Optional callback invoked when a web view is first created. [controller] is
/// the [WebViewController] for the created web view.
typedef void WebViewCreatedCallback(WebViewController controller);

/// Describes the state of JavaScript support in a given web view.


enum JavascriptMode {
/// JavaScript execution is disabled.
disabled,

/// JavaScript execution is not restricted.


unrestricted,
}

/// A message that was sent by JavaScript code running in a [WebView].


class JavascriptMessage {
/// Constructs a JavaScript message object.
///
/// The `message` parameter must not be null.
const JavascriptMessage(this.message) : assert(message != null);

/// The contents of the message that was sent by the JavaScript code.
final String message;
}

/// Callback type for handling messages sent from Javascript running in a web vi
ew.
typedef void JavascriptMessageHandler(JavascriptMessage message);

/// Information about a navigation action that is about to be executed.


class NavigationRequest {
NavigationRequest._({this.url, this.isForMainFrame});

/// The URL that will be loaded if the navigation is executed.


final String url;

/// Whether the navigation request is to be loaded as the main frame.


final bool isForMainFrame;

@override
String toString() {
return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
}
}

/// A decision on how to handle a navigation request.


enum NavigationDecision {
/// Prevent the navigation from taking place.
prevent,
/// Allow the navigation to take place.
navigate,
}

/// Decides how to handle a specific navigation request.


///
/// The returned [NavigationDecision] determines how the navigation described by
/// `navigation` should be handled.
///
/// See also: [WebView.navigationDelegate].
typedef FutureOr<NavigationDecision> NavigationDelegate(
NavigationRequest navigation);

/// Signature for when a [WebView] has started loading a page.


typedef void PageStartedCallback(String url);

/// Signature for when a [WebView] has finished loading a page.


typedef void PageFinishedCallback(String url);

/// Signature for when a [WebView] has failed to load a resource.


typedef void WebResourceErrorCallback(WebResourceError error);

/// Specifies possible restrictions on automatic media playback.


///
/// This is typically used in [WebView.initialMediaPlaybackPolicy].
// The method channel implementation is marshalling this enum to the value's ind
ex, so the order
// is important.
enum AutoMediaPlaybackPolicy {
/// Starting any kind of media playback requires a user action.
///
/// For example: JavaScript code cannot start playing media unless the code wa
s executed
/// as a result of a user action (like a touch event).
require_user_action_for_all_media_types,

/// Starting any kind of media playback is always allowed.


///
/// For example: JavaScript code that's triggered when the page is loaded can
start playing
/// video or audio.
always_allow,
}

final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$');

/// A named channel for receiving messaged from JavaScript code running inside a
web view.
class JavascriptChannel {
/// Constructs a Javascript channel.
///
/// The parameters `name` and `onMessageReceived` must not be null.
JavascriptChannel({
@required this.name,
@required this.onMessageReceived,
}) : assert(name != null),
assert(onMessageReceived != null),
assert(_validChannelNames.hasMatch(name));

/// The channel's name.


///
/// Passing this channel object as part of a [WebView.javascriptChannels] adds
a channel object to
/// the Javascript window object's property named `name`.
///
/// The name must start with a letter or underscore(_), followed by any combin
ation of those
/// characters plus digits.
///
/// Note that any JavaScript existing `window` property with this name will be
overriden.
///
/// See also [WebView.javascriptChannels] for more details on the channel regi
stration mechanism.
final String name;

/// A callback that's invoked when a message is received through the channel.
final JavascriptMessageHandler onMessageReceived;
}

/// A web view widget for showing html content.


class WebView extends StatefulWidget {
/// Creates a new web view.
///
/// The web view can be controlled using a `WebViewController` that is passed
to the
/// `onWebViewCreated` callback once the web view is created.
///
/// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be
null.
const WebView({
Key key,
this.onWebViewCreated,
this.initialUrl,
this.javascriptMode = JavascriptMode.disabled,
this.javascriptChannels,
this.navigationDelegate,
this.gestureRecognizers,
this.onPageStarted,
this.onPageFinished,
this.onWebResourceError,
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
this.userAgent,
this.initialMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
}) : assert(javascriptMode != null),
assert(initialMediaPlaybackPolicy != null),
super(key: key);

static WebViewPlatform _platform;

/// Sets a custom [WebViewPlatform].


///
/// This property can be set to use a custom platform implementation for WebVi
ews.
///
/// Setting `platform` doesn't affect [WebView]s that were already created.
///
/// The default value is [AndroidWebView] on Android and [CupertinoWebView] on
iOS.
static set platform(WebViewPlatform platform) {
_platform = platform;
}

/// The WebView platform that's used by this WebView.


///
/// The default value is [AndroidWebView] on Android and [CupertinoWebView] on
iOS.
static WebViewPlatform get platform {
if (_platform == null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
_platform = AndroidWebView();
break;
case TargetPlatform.iOS:
_platform = CupertinoWebView();
break;
default:
throw UnsupportedError(
"Trying to use the default webview implementation for $defaultTarg
etPlatform but there isn't a default one");
}
}
return _platform;
}

/// If not null invoked once the web view is created.


final WebViewCreatedCallback onWebViewCreated;

/// 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
/// events, e.g if the web view is inside a [ListView] the [ListView] will wan
t to handle
/// vertical drags. The web view will claim gestures that are recognized by an
y of the
/// recognizers on this list.
///
/// When this set is empty or null, the web view will only handle pointer even
ts for gestures that
/// were not claimed by any other gesture recognizer.
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;

/// The initial URL to load.


final String initialUrl;

/// Whether Javascript execution is enabled.


final JavascriptMode javascriptMode;

/// The set of [JavascriptChannel]s available to JavaScript code running in th


e web view.
///
/// For each [JavascriptChannel] in the set, a channel object is made availabl
e for the
/// JavaScript code in a window property named [JavascriptChannel.name].
/// The JavaScript code can then call `postMessage` on that object to send a m
essage that will be
/// passed to [JavascriptChannel.onMessageReceived].
///
/// For example for the following JavascriptChannel:
///
/// ```dart
/// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage mes
sage) { print(message.message); });
/// ```
///
/// JavaScript code can call:
///
/// ```javascript
/// Print.postMessage('Hello');
/// ```
///
/// To asynchronously invoke the message handler which will print the message
to standard output.
///
/// Adding a new JavaScript channel only takes affect after the next page is l
oaded.
///
/// Set values must not be null. A [JavascriptChannel.name] cannot be the same
for multiple
/// channels in the list.
///
/// A null value is equivalent to an empty set.
final Set<JavascriptChannel> javascriptChannels;

/// A delegate function that decides how to handle navigation actions.


///
/// When a navigation is initiated by the WebView (e.g when a user clicks a li
nk)
/// this delegate is called and has to decide how to proceed with the navigati
on.
///
/// See [NavigationDecision] for possible decisions the delegate can take.
///
/// When null all navigation actions are allowed.
///
/// Caveats on Android:
///
/// * Navigation actions targeted to the main frame can be intercepted,
/// navigation actions targeted to subframes are allowed regardless of the
value
/// returned by this delegate.
/// * Setting a navigationDelegate makes the WebView treat all navigations a
s if they were
/// triggered by a user gesture, this disables some of Chromium's security
mechanisms.
/// A navigationDelegate should only be set when loading trusted content.
/// * On Android WebView versions earlier than 67(most devices running at le
ast Android L+ should have
/// a later version):
/// * When a navigationDelegate is set pages with frames are not properly
handled by the
/// webview, and frames will be opened in the main frame.
/// * When a navigationDelegate is set HTTP requests do not include the HT
TP referer header.
final NavigationDelegate navigationDelegate;

/// Invoked when a page starts loading.


final PageStartedCallback onPageStarted;
/// Invoked when a page has finished loading.
///
/// This is invoked only for the main frame.
///
/// When [onPageFinished] is invoked on Android, the page being rendered may
/// not be updated yet.
///
/// When invoked on iOS or Android, any Javascript code that is embedded
/// directly in the HTML has been loaded and code injected with
/// [WebViewController.evaluateJavascript] can assume this.
final PageFinishedCallback onPageFinished;

/// Invoked when a web resource has failed to load.


///
/// This can be called for any resource (iframe, image, etc.), not just for
/// the main page.
final WebResourceErrorCallback onWebResourceError;

/// Controls whether WebView debugging is enabled.


///
/// Setting this to true enables [WebView debugging on Android](https://develo
pers.google.com/web/tools/chrome-devtools/remote-debugging/).
///
/// WebView debugging is enabled by default in dev builds on iOS.
///
/// To debug WebViews on iOS:
/// - Enable developer options (Open Safari, go to Preferences -> Advanced and
make sure "Show Develop Menu in Menubar" is on.)
/// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> <you
r webview page>
///
/// By default `debuggingEnabled` is false.
final bool debuggingEnabled;

/// A Boolean value indicating whether horizontal swipe gestures will trigger
back-forward list navigations.
///
/// This only works on iOS.
///
/// By default `gestureNavigationEnabled` is false.
final bool gestureNavigationEnabled;

/// The value used for the HTTP User-Agent: request header.
///
/// When null the platform's webview default is used for the User-Agent header
.
///
/// When the [WebView] is rebuilt with a different `userAgent`, the page reloa
ds and the request uses the new User Agent.
///
/// When [WebViewController.goBack] is called after changing `userAgent` the p
revious `userAgent` value is used until the page is reloaded.
///
/// This field is ignored on iOS versions prior to 9 as the platform does not
support a custom
/// user agent.
///
/// By default `userAgent` is null.
final String userAgent;

/// Which restrictions apply on automatic media playback.


///
/// This initial value is applied to the platform's webview upon creation. Any
following
/// changes to this parameter are ignored (as long as the state of the [WebVie
w] is preserved).
///
/// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all
_media_types].
final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy;

@override
State<StatefulWidget> createState() => _WebViewState();
}

class _WebViewState extends State<WebView> {


final Completer<WebViewController> _controller =
Completer<WebViewController>();

_PlatformCallbacksHandler _platformCallbacksHandler;

@override
Widget build(BuildContext context) {
return WebView.platform.build(
context: context,
onWebViewPlatformCreated: _onWebViewPlatformCreated,
webViewPlatformCallbacksHandler: _platformCallbacksHandler,
gestureRecognizers: widget.gestureRecognizers,
creationParams: _creationParamsfromWidget(widget),
);
}

@override
void initState() {
super.initState();
_assertJavascriptChannelNamesAreUnique();
_platformCallbacksHandler = _PlatformCallbacksHandler(widget);
}

@override
void didUpdateWidget(WebView oldWidget) {
super.didUpdateWidget(oldWidget);
_assertJavascriptChannelNamesAreUnique();
_controller.future.then((WebViewController controller) {
_platformCallbacksHandler._widget = widget;
controller._updateWidget(widget);
});
}

void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) {


final WebViewController controller =
WebViewController._(widget, webViewPlatform, _platformCallbacksHandler);
_controller.complete(controller);
if (widget.onWebViewCreated != null) {
widget.onWebViewCreated(controller);
}
}

void _assertJavascriptChannelNamesAreUnique() {
if (widget.javascriptChannels == null ||
widget.javascriptChannels.isEmpty) {
return;
}
assert(_extractChannelNames(widget.javascriptChannels).length ==
widget.javascriptChannels.length);
}
}

CreationParams _creationParamsfromWidget(WebView widget) {


return CreationParams(
initialUrl: widget.initialUrl,
webSettings: _webSettingsFromWidget(widget),
javascriptChannelNames: _extractChannelNames(widget.javascriptChannels),
userAgent: widget.userAgent,
autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
);
}

WebSettings _webSettingsFromWidget(WebView widget) {


return WebSettings(
javascriptMode: widget.javascriptMode,
hasNavigationDelegate: widget.navigationDelegate != null,
debuggingEnabled: widget.debuggingEnabled,
gestureNavigationEnabled: widget.gestureNavigationEnabled,
userAgent: WebSetting<String>.of(widget.userAgent),
);
}

// This method assumes that no fields in `currentValue` are null.


WebSettings _clearUnchangedWebSettings(
WebSettings currentValue, WebSettings newValue) {
assert(currentValue.javascriptMode != null);
assert(currentValue.hasNavigationDelegate != null);
assert(currentValue.debuggingEnabled != null);
assert(currentValue.userAgent.isPresent);
assert(newValue.javascriptMode != null);
assert(newValue.hasNavigationDelegate != null);
assert(newValue.debuggingEnabled != null);
assert(newValue.userAgent.isPresent);

JavascriptMode javascriptMode;
bool hasNavigationDelegate;
bool debuggingEnabled;
WebSetting<String> userAgent = WebSetting<String>.absent();
if (currentValue.javascriptMode != newValue.javascriptMode) {
javascriptMode = newValue.javascriptMode;
}
if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
hasNavigationDelegate = newValue.hasNavigationDelegate;
}
if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
debuggingEnabled = newValue.debuggingEnabled;
}
if (currentValue.userAgent != newValue.userAgent) {
userAgent = newValue.userAgent;
}

return WebSettings(
javascriptMode: javascriptMode,
hasNavigationDelegate: hasNavigationDelegate,
debuggingEnabled: debuggingEnabled,
userAgent: userAgent,
);
}
Set<String> _extractChannelNames(Set<JavascriptChannel> channels) {
final Set<String> channelNames = channels == null
// TODO(iskakaushik): Remove this when collection literals makes it to sta
ble.
// ignore: prefer_collection_literals
? Set<String>()
: channels.map((JavascriptChannel channel) => channel.name).toSet();
return channelNames;
}

class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {


_PlatformCallbacksHandler(this._widget) {
_updateJavascriptChannelsFromSet(_widget.javascriptChannels);
}

WebView _widget;

// Maps a channel name to a channel.


final Map<String, JavascriptChannel> _javascriptChannels =
<String, JavascriptChannel>{};

@override
void onJavaScriptChannelMessage(String channel, String message) {
_javascriptChannels[channel].onMessageReceived(JavascriptMessage(message));
}

@override
FutureOr<bool> onNavigationRequest({String url, bool isForMainFrame}) async {
final NavigationRequest request =
NavigationRequest._(url: url, isForMainFrame: isForMainFrame);
final bool allowNavigation = _widget.navigationDelegate == null ||
await _widget.navigationDelegate(request) ==
NavigationDecision.navigate;
return allowNavigation;
}

@override
void onPageStarted(String url) {
if (_widget.onPageStarted != null) {
_widget.onPageStarted(url);
}
}

@override
void onPageFinished(String url) {
if (_widget.onPageFinished != null) {
_widget.onPageFinished(url);
}
}

@override
void onWebResourceError(WebResourceError error) {
if (_widget.onWebResourceError != null) {
_widget.onWebResourceError(error);
}
}

void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) {


_javascriptChannels.clear();
if (channels == null) {
return;
}
for (JavascriptChannel channel in channels) {
_javascriptChannels[channel.name] = channel;
}
}
}
/// Controls a [WebView].
///
/// A [WebViewController] instance can be obtained by setting the [WebView.onWeb
ViewCreated]
/// callback for a [WebView] widget.
class WebViewController {
WebViewController._(
this._widget,
this._webViewPlatformController,
this._platformCallbacksHandler,
) : assert(_webViewPlatformController != null) {
_settings = _webSettingsFromWidget(_widget);
}

final WebViewPlatformController _webViewPlatformController;

final _PlatformCallbacksHandler _platformCallbacksHandler;

WebSettings _settings;

WebView _widget;

/// Loads the specified URL.


///
/// If `headers` is not null and the URL is an HTTP URL, the key value paris i
n `headers` will
/// be added as key value pairs of HTTP headers for the request.
///
/// `url` must not be null.
///
/// Throws an ArgumentError if `url` is not a valid URL string.
Future<void> loadUrl(
String url, {
Map<String, String> headers,
}) async {
assert(url != null);
_validateUrlString(url);
return _webViewPlatformController.loadUrl(url, headers);
}

/// Accessor to the current URL that the WebView is displaying.


///
/// If [WebView.initialUrl] was never specified, returns `null`.
/// Note that this operation is asynchronous, and it is possible that the
/// current URL changes again by the time this function returns (in other
/// words, by the time this future completes, the WebView may be displaying a
/// different URL).
Future<String> currentUrl() {
return _webViewPlatformController.currentUrl();
}

/// Checks whether there's a back history item.


///
/// Note that this operation is asynchronous, and it is possible that the "can
GoBack" state has
/// changed by the time the future completed.
Future<bool> canGoBack() {
return _webViewPlatformController.canGoBack();
}

/// Checks whether there's a forward history item.


///
/// Note that this operation is asynchronous, and it is possible that the "can
GoForward" state has
/// changed by the time the future completed.
Future<bool> canGoForward() {
return _webViewPlatformController.canGoForward();
}

/// Goes back in the history of this WebView.


///
/// If there is no back history item this is a no-op.
Future<void> goBack() {
return _webViewPlatformController.goBack();
}

/// Goes forward in the history of this WebView.


///
/// If there is no forward history item this is a no-op.
Future<void> goForward() {
return _webViewPlatformController.goForward();
}

/// Reloads the current URL.


Future<void> reload() {
return _webViewPlatformController.reload();
}

/// Clears all caches used by the [WebView].


///
/// The following caches are cleared:
/// 1. Browser HTTP Cache.
/// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-an
d-offline/web-storage/cache-api) caches.
/// These are not yet supported in iOS WkWebView. Service workers tend to u
se this cache.
/// 3. Application cache.
/// 4. Local Storage.
///
/// Note: Calling this method also triggers a reload.
Future<void> clearCache() async {
await _webViewPlatformController.clearCache();
return reload();
}

Future<void> _updateWidget(WebView widget) async {


_widget = widget;
await _updateSettings(_webSettingsFromWidget(widget));
await _updateJavascriptChannels(widget.javascriptChannels);
}

Future<void> _updateSettings(WebSettings newSettings) {


final WebSettings update =
_clearUnchangedWebSettings(_settings, newSettings);
_settings = newSettings;
return _webViewPlatformController.updateSettings(update);
}
Future<void> _updateJavascriptChannels(
Set<JavascriptChannel> newChannels) async {
final Set<String> currentChannels =
_platformCallbacksHandler._javascriptChannels.keys.toSet();
final Set<String> newChannelNames = _extractChannelNames(newChannels);
final Set<String> channelsToAdd =
newChannelNames.difference(currentChannels);
final Set<String> channelsToRemove =
currentChannels.difference(newChannelNames);
if (channelsToRemove.isNotEmpty) {
await _webViewPlatformController
.removeJavascriptChannels(channelsToRemove);
}
if (channelsToAdd.isNotEmpty) {
await _webViewPlatformController.addJavascriptChannels(channelsToAdd);
}
_platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels);
}

/// Evaluates a JavaScript expression in the context of the current page.


///
/// On Android returns the evaluation result as a JSON formatted string.
///
/// On iOS depending on the value type the return value would be one of:
///
/// - For primitive JavaScript types: the value string formatted (e.g JavaScr
ipt 100 returns '100').
/// - For JavaScript arrays of supported types: a string formatted NSArray(e.
g '(1,2,3), note that the string for NSArray is formatted and might contain newl
ines and extra spaces.').
/// - Other non-primitive types are not supported on iOS and will complete th
e Future with an error.
///
/// The Future completes with an error if a JavaScript error occurred, or on i
OS, if the type of the
/// evaluated expression is not supported as described above.
///
/// When evaluating Javascript in a [WebView], it is best practice to wait for
/// the [WebView.onPageFinished] callback. This guarantees all the Javascript
/// embedded in the main frame HTML has been loaded.
Future<String> evaluateJavascript(String javascriptString) {
if (_settings.javascriptMode == JavascriptMode.disabled) {
return Future<String>.error(FlutterError(
'JavaScript mode must be enabled/unrestricted when calling evaluateJav
ascript.'));
}
if (javascriptString == null) {
return Future<String>.error(
ArgumentError('The argument javascriptString must not be null.'));
}
// TODO(amirh): remove this on when the invokeMethod update makes it to stab
le Flutter.
// https://github.com/flutter/flutter/issues/26431
// ignore: strong_mode_implicit_dynamic_method
return _webViewPlatformController.evaluateJavascript(javascriptString);
}

/// Returns the title of the currently loaded page.


Future<String> getTitle() {
return _webViewPlatformController.getTitle();
}
/// Sets the WebView's content scroll position.
///
/// The parameters `x` and `y` specify the scroll position in WebView pixels.
Future<void> scrollTo(int x, int y) {
return _webViewPlatformController.scrollTo(x, y);
}
/// Move the scrolled position of this view.
///
/// The parameters `x` and `y` specify the amount of WebView pixels to scroll
by horizontally and vertically respectively.
Future<void> scrollBy(int x, int y) {
return _webViewPlatformController.scrollBy(x, y);
}

/// Return the horizontal scroll position, in WebView pixels, of this view.
///
/// Scroll position is measured from left.
Future<int> getScrollX() {
return _webViewPlatformController.getScrollX();
}

/// Return the vertical scroll position, in WebView pixels, of this view.
///
/// Scroll position is measured from top.
Future<int> getScrollY() {
return _webViewPlatformController.getScrollY();
}
}

/// Manages cookies pertaining to all [WebView]s.


class CookieManager {
/// Creates a [CookieManager] -- returns the instance if it's already been cal
led.
factory CookieManager() {
return _instance ??= CookieManager._();
}

CookieManager._();

static CookieManager _instance;

/// Clears all cookies for all [WebView] instances.


///
/// This is a no op on iOS version smaller than 9.
///
/// Returns true if cookies were present before clearing, else false.
Future<bool> clearCookies() => WebView.platform.clearCookies();
}

// Throws an ArgumentError if `url` is not a valid URL string.


void _validateUrlString(String url) {
try {
final Uri uri = Uri.parse(url);
if (uri.scheme.isEmpty) {
throw ArgumentError('Missing scheme in URL string: "$url"');
}
} on FormatException catch (e) {
throw ArgumentError(e);
}
}

You might also like