flutter_inappbrowser.dart 52.6 KB
Newer Older
pichillilorenzo's avatar
pichillilorenzo committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

pichillilorenzo's avatar
pichillilorenzo committed
22
import 'dart:io';
pichillilorenzo's avatar
pichillilorenzo committed
23
import 'dart:async';
24
import 'dart:collection';
25
import 'dart:typed_data';
26
import 'dart:convert';
pichillilorenzo's avatar
pichillilorenzo committed
27

pichillilorenzo's avatar
pichillilorenzo committed
28 29
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
pichillilorenzo's avatar
pichillilorenzo committed
30
import 'package:flutter/services.dart';
pichillilorenzo's avatar
pichillilorenzo committed
31 32
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
33
import 'package:uuid/uuid.dart';
pichillilorenzo's avatar
pichillilorenzo committed
34
import 'package:mime/mime.dart';
35 36

typedef Future<dynamic> ListenerCallback(MethodCall call);
37
typedef Future<void> JavaScriptHandlerCallback(List<dynamic> arguments);
38

39 40 41 42 43 44 45
var _uuidGenerator = new Uuid();

///
enum ConsoleMessageLevel {
  DEBUG, ERROR, LOG, TIP, WARNING
}

46 47
///Public class representing a resource request of the [InAppBrowser] WebView.
///It is used by the method [InAppBrowser.onLoadResource()].
48 49 50 51 52 53 54 55 56 57
class WebResourceRequest {

  String url;
  Map<String, String> headers;
  String method;

  WebResourceRequest(this.url, this.headers, this.method);

}

58 59
///Public class representing a resource response of the [InAppBrowser] WebView.
///It is used by the method [InAppBrowser.onLoadResource()].
60 61 62 63 64
class WebResourceResponse {

  String url;
  Map<String, String> headers;
  int statusCode;
65 66
  int startTime;
  int duration;
67 68
  Uint8List data;

69
  WebResourceResponse(this.url, this.headers, this.statusCode, this.startTime, this.duration, this.data);
70 71 72

}

73 74 75 76 77 78 79 80 81 82 83 84 85
///Public class representing a JavaScript console message from WebCore.
///This could be a issued by a call to one of the console logging functions (e.g. console.log('...')) or a JavaScript error on the page.
///
///To receive notifications of these messages, override the [InAppBrowser.onConsoleMessage()] function.
class ConsoleMessage {

  String sourceURL = "";
  int lineNumber = 1;
  String message = "";
  ConsoleMessageLevel messageLevel = ConsoleMessageLevel.LOG;

  ConsoleMessage(this.sourceURL, this.lineNumber, this.message, this.messageLevel);
}
pichillilorenzo's avatar
pichillilorenzo committed
86

87 88
class _ChannelManager {
  static const MethodChannel channel = const MethodChannel('com.pichillilorenzo/flutter_inappbrowser');
89
  static bool initialized = false;
90
  static final listeners = HashMap<String, ListenerCallback>();
91 92

  static Future<dynamic> _handleMethod(MethodCall call) async {
93
    String uuid = call.arguments["uuid"];
pichillilorenzo's avatar
pichillilorenzo committed
94
    return await listeners[uuid](call);
95 96
  }

pichillilorenzo's avatar
pichillilorenzo committed
97
  static void addListener(String key, ListenerCallback callback) {
98 99
    if (!initialized)
      init();
100
    listeners.putIfAbsent(key, () => callback);
101 102 103 104
  }

  static void init () {
    channel.setMethodCallHandler(_handleMethod);
105
    initialized = true;
106 107 108
  }
}

pichillilorenzo's avatar
pichillilorenzo committed
109
///InAppBrowser class. [webViewController] can be used to access the [InAppWebView] API.
110
///
111
///This class uses the native WebView of the platform.
pichillilorenzo's avatar
pichillilorenzo committed
112 113
class InAppBrowser {

114
  String uuid;
115
  Map<String, List<JavaScriptHandlerCallback>> javaScriptHandlersMap = HashMap<String, List<JavaScriptHandlerCallback>>();
pichillilorenzo's avatar
pichillilorenzo committed
116
  bool _isOpened = false;
pichillilorenzo's avatar
pichillilorenzo committed
117 118
  /// WebView Controller that can be used to access the [InAppWebView] API.
  InAppWebViewController webViewController;
119

pichillilorenzo's avatar
pichillilorenzo committed
120 121
  ///
  InAppBrowser () {
122
    uuid = _uuidGenerator.v4();
123
    _ChannelManager.addListener(uuid, _handleMethod);
pichillilorenzo's avatar
pichillilorenzo committed
124
    _isOpened = false;
pichillilorenzo's avatar
pichillilorenzo committed
125
    webViewController = new InAppWebViewController.fromInAppBrowser(uuid, _ChannelManager.channel, this);
pichillilorenzo's avatar
pichillilorenzo committed
126 127 128 129
  }

  Future<dynamic> _handleMethod(MethodCall call) async {
    switch(call.method) {
130
      case "onExit":
pichillilorenzo's avatar
pichillilorenzo committed
131
        this._isOpened = false;
pichillilorenzo's avatar
pichillilorenzo committed
132 133
        onExit();
        break;
pichillilorenzo's avatar
pichillilorenzo committed
134
      default:
pichillilorenzo's avatar
pichillilorenzo committed
135
        return webViewController._handleMethod(call);
pichillilorenzo's avatar
pichillilorenzo committed
136 137 138
    }
  }

pichillilorenzo's avatar
pichillilorenzo committed
139
  ///Opens an [url] in a new [InAppBrowser] instance.
pichillilorenzo's avatar
pichillilorenzo committed
140
  ///
141
  ///- [url]: The [url] to load. Call [encodeUriComponent()] on this if the [url] contains Unicode characters. The default value is `about:blank`.
pichillilorenzo's avatar
pichillilorenzo committed
142
  ///
143
  ///- [headers]: The additional headers to be used in the HTTP request for this URL, specified as a map from name to value.
pichillilorenzo's avatar
pichillilorenzo committed
144
  ///
145
  ///- [options]: Options for the [InAppBrowser].
pichillilorenzo's avatar
pichillilorenzo committed
146
  ///
pichillilorenzo's avatar
pichillilorenzo committed
147 148 149 150
  ///  - All platforms support:
  ///    - __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`.
  ///    - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`.
pichillilorenzo's avatar
pichillilorenzo committed
151
  ///    - __userAgent__: Set the custom WebView's user-agent.
pichillilorenzo's avatar
pichillilorenzo committed
152 153 154 155
  ///    - __javaScriptEnabled__: Set to `true` to enable JavaScript. The default value is `true`.
  ///    - __javaScriptCanOpenWindowsAutomatically__: Set to `true` to allow JavaScript open windows without user interaction. The default value is `false`.
  ///    - __hidden__: Set to `true` to create the browser and load the page, but not show it. The `onLoadStop` event fires when loading is complete. Omit or set to `false` (default) to have the browser open and load normally.
  ///    - __toolbarTop__: Set to `false` to hide the toolbar at the top of the WebView. The default value is `true`.
pichillilorenzo's avatar
pichillilorenzo committed
156
  ///    - __toolbarTopBackgroundColor__: Set the custom background color of the toolbar at the top.
pichillilorenzo's avatar
pichillilorenzo committed
157 158
  ///    - __hideUrlBar__: Set to `true` to hide the url bar on the toolbar at the top. The default value is `false`.
  ///    - __mediaPlaybackRequiresUserGesture__: Set to `true` to prevent HTML5 audio or video from autoplaying. The default value is `true`.
pichillilorenzo's avatar
pichillilorenzo committed
159
  ///
pichillilorenzo's avatar
pichillilorenzo committed
160
  ///  - **Android** supports these additional options:
pichillilorenzo's avatar
pichillilorenzo committed
161
  ///
pichillilorenzo's avatar
pichillilorenzo committed
162 163 164 165 166 167 168 169 170 171
  ///    - __hideTitleBar__: Set to `true` if you want the title should be displayed. The default value is `false`.
  ///    - __closeOnCannotGoBack__: Set to `false` to not close the InAppBrowser when the user click on the back button and the WebView cannot go back to the history. The default value is `true`.
  ///    - __clearSessionCache__: Set to `true` to have the session cookie cache cleared before the new window is opened.
  ///    - __builtInZoomControls__: Set to `true` if the WebView should use its built-in zoom mechanisms. The default value is `false`.
  ///    - __supportZoom__: Set to `false` if the WebView should not support zooming using its on-screen zoom controls and gestures. The default value is `true`.
  ///    - __databaseEnabled__: Set to `true` if you want the database storage API is enabled. The default value is `false`.
  ///    - __domStorageEnabled__: Set to `true` if you want the DOM storage API is enabled. The default value is `false`.
  ///    - __useWideViewPort__: Set to `true` if the WebView should enable support for the "viewport" HTML meta tag or should use a wide viewport. When the value of the setting is false, the layout width is always set to the width of the WebView control in device-independent (CSS) pixels. When the value is true and the page contains the viewport meta tag, the value of the width specified in the tag is used. If the page does not contain the tag or does not provide a width, then a wide viewport will be used. The default value is `true`.
  ///    - __safeBrowsingEnabled__: Set to `true` if you want the Safe Browsing is enabled. Safe Browsing allows WebView to protect against malware and phishing attacks by verifying the links. The default value is `true`.
  ///    - __progressBar__: Set to `false` to hide the progress bar at the bottom of the toolbar at the top. The default value is `true`.
pichillilorenzo's avatar
pichillilorenzo committed
172
  ///
pichillilorenzo's avatar
pichillilorenzo committed
173
  ///  - **iOS** supports these additional options:
pichillilorenzo's avatar
pichillilorenzo committed
174
  ///
pichillilorenzo's avatar
pichillilorenzo committed
175 176
  ///    - __disallowOverScroll__: Set to `true` to disable the bouncing of the WebView when the scrolling has reached an edge of the content. The default value is `false`.
  ///    - __toolbarBottom__: Set to `false` to hide the toolbar at the bottom of the WebView. The default value is `true`.
pichillilorenzo's avatar
pichillilorenzo committed
177
  ///    - __toolbarBottomBackgroundColor__: Set the custom background color of the toolbar at the bottom.
pichillilorenzo's avatar
pichillilorenzo committed
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  ///    - __toolbarBottomTranslucent__: Set to `true` to set the toolbar at the bottom translucent. The default value is `true`.
  ///    - __closeButtonCaption__: Set the custom text for the close button.
  ///    - __closeButtonColor__: Set the custom color for the close button.
  ///    - __presentationStyle__: Set the custom modal presentation style when presenting the WebView. The default value is `0 //fullscreen`. See [UIModalPresentationStyle](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle) for all the available styles.
  ///    - __transitionStyle__: Set to the custom transition style when presenting the WebView. The default value is `0 //crossDissolve`. See [UIModalTransitionStyle](https://developer.apple.com/documentation/uikit/uimodaltransitionStyle) for all the available styles.
  ///    - __enableViewportScale__: Set to `true` to allow a viewport meta tag to either disable or restrict the range of user scaling. The default value is `false`.
  ///    - __suppressesIncrementalRendering__: Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory.. The default value is `false`.
  ///    - __allowsAirPlayForMediaPlayback__: Set to `true` to allow AirPlay. The default value is `true`.
  ///    - __allowsBackForwardNavigationGestures__: Set to `true` to allow the horizontal swipe gestures trigger back-forward list navigations. The default value is `true`.
  ///    - __allowsLinkPreview__: Set to `true` to allow that pressing on a link displays a preview of the destination for the link. The default value is `true`.
  ///    - __ignoresViewportScaleLimits__: Set to `true` if you want that the WebView should always allow scaling of the webpage, regardless of the author's intent. The ignoresViewportScaleLimits property overrides the `user-scalable` HTML property in a webpage. The default value is `false`.
  ///    - __allowsInlineMediaPlayback__: Set to `true` to allow HTML5 media playback to appear inline within the screen layout, using browser-supplied controls rather than native controls. For this to work, add the `webkit-playsinline` attribute to any `<video>` elements. The default value is `false`.
  ///    - __allowsPictureInPictureMediaPlayback__: Set to `true` to allow HTML5 videos play picture-in-picture. The default value is `true`.
  ///    - __spinner__: Set to `false` to hide the spinner when the WebView is loading a page. The default value is `true`.
  Future<void> open({String url = "about:blank", Map<String, String> headers = const {}, Map<String, dynamic> options = const {}}) async {
pichillilorenzo's avatar
pichillilorenzo committed
193
    assert(url != null && url.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
194
    this._throwIsAlreadyOpened(message: 'Cannot open $url!');
pichillilorenzo's avatar
pichillilorenzo committed
195
    Map<String, dynamic> args = <String, dynamic>{};
196
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
197
    args.putIfAbsent('url', () => url);
198
    args.putIfAbsent('headers', () => headers);
pichillilorenzo's avatar
pichillilorenzo committed
199
    args.putIfAbsent('options', () => options);
pichillilorenzo's avatar
pichillilorenzo committed
200 201
    args.putIfAbsent('openWithSystemBrowser', () => false);
    args.putIfAbsent('isLocalFile', () => false);
202
    args.putIfAbsent('useChromeSafariBrowser', () => false);
pichillilorenzo's avatar
pichillilorenzo committed
203 204 205 206
    await _ChannelManager.channel.invokeMethod('open', args);
    this._isOpened = true;
  }

pichillilorenzo's avatar
pichillilorenzo committed
207
  ///Opens the giver [assetFilePath] file in a new [InAppBrowser] instance. The other arguments are the same of [InAppBrowser.open()].
pichillilorenzo's avatar
pichillilorenzo committed
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
  ///
  ///To be able to load your local files (assets, js, css, etc.), you need to add them in the `assets` section of the `pubspec.yaml` file, otherwise they cannot be found!
  ///
  ///Example of a `pubspec.yaml` file:
  ///```yaml
  ///...
  ///
  ///# The following section is specific to Flutter.
  ///flutter:
  ///
  ///  # The following line ensures that the Material Icons font is
  ///  # included with your application, so that you can use the icons in
  ///  # the material Icons class.
  ///  uses-material-design: true
  ///
  ///  assets:
  ///    - assets/index.html
  ///    - assets/css/
  ///    - assets/images/
  ///
  ///...
  ///```
  ///Example of a `main.dart` file:
  ///```dart
  ///...
pichillilorenzo's avatar
pichillilorenzo committed
233
  ///inAppBrowser.openFile("assets/index.html");
pichillilorenzo's avatar
pichillilorenzo committed
234 235
  ///...
  ///```
pichillilorenzo's avatar
pichillilorenzo committed
236
  Future<void> openFile(String assetFilePath, {Map<String, String> headers = const {}, Map<String, dynamic> options = const {}}) async {
pichillilorenzo's avatar
pichillilorenzo committed
237
    assert(assetFilePath != null && assetFilePath.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
238
    this._throwIsAlreadyOpened(message: 'Cannot open $assetFilePath!');
pichillilorenzo's avatar
pichillilorenzo committed
239 240 241 242
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => uuid);
    args.putIfAbsent('url', () => assetFilePath);
    args.putIfAbsent('headers', () => headers);
pichillilorenzo's avatar
pichillilorenzo committed
243 244 245 246 247 248 249 250 251 252
    args.putIfAbsent('options', () => options);
    args.putIfAbsent('openWithSystemBrowser', () => false);
    args.putIfAbsent('isLocalFile', () => true);
    args.putIfAbsent('useChromeSafariBrowser', () => false);
    await _ChannelManager.channel.invokeMethod('open', args);
    this._isOpened = true;
  }

  ///This is a static method that opens an [url] in the system browser. You wont be able to use the [InAppBrowser] methods here!
  static Future<void> openWithSystemBrowser(String url) async {
pichillilorenzo's avatar
pichillilorenzo committed
253
    assert(url != null && url.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
254 255 256 257 258 259 260 261
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => "");
    args.putIfAbsent('url', () => url);
    args.putIfAbsent('headers', () => {});
    args.putIfAbsent('isLocalFile', () => false);
    args.putIfAbsent('openWithSystemBrowser', () => true);
    args.putIfAbsent('useChromeSafariBrowser', () => false);
    return await _ChannelManager.channel.invokeMethod('open', args);
262 263
  }

pichillilorenzo's avatar
pichillilorenzo committed
264 265
  ///Displays an [InAppBrowser] window that was opened hidden. Calling this has no effect if the [InAppBrowser] was already visible.
  Future<void> show() async {
pichillilorenzo's avatar
pichillilorenzo committed
266
    this._throwIsNotOpened();
267 268
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
269
    await _ChannelManager.channel.invokeMethod('show', args);
pichillilorenzo's avatar
pichillilorenzo committed
270 271 272 273
  }

  ///Hides the [InAppBrowser] window. Calling this has no effect if the [InAppBrowser] was already hidden.
  Future<void> hide() async {
pichillilorenzo's avatar
pichillilorenzo committed
274
    this._throwIsNotOpened();
275 276
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
277
    await _ChannelManager.channel.invokeMethod('hide', args);
pichillilorenzo's avatar
pichillilorenzo committed
278 279 280 281
  }

  ///Closes the [InAppBrowser] window.
  Future<void> close() async {
pichillilorenzo's avatar
pichillilorenzo committed
282
    this._throwIsNotOpened();
283 284
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
285
    await _ChannelManager.channel.invokeMethod('close', args);
pichillilorenzo's avatar
pichillilorenzo committed
286 287
  }

288 289
  ///Check if the Web View of the [InAppBrowser] instance is hidden.
  Future<bool> isHidden() async {
pichillilorenzo's avatar
pichillilorenzo committed
290
    this._throwIsNotOpened();
291 292 293
    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('uuid', () => uuid);
    return await _ChannelManager.channel.invokeMethod('isHidden', args);
294 295
  }

pichillilorenzo's avatar
pichillilorenzo committed
296 297
  ///Sets the [InAppBrowser] options with the new [options] and evaluates them.
  Future<void> setOptions(Map<String, dynamic> options) async {
pichillilorenzo's avatar
pichillilorenzo committed
298
    this._throwIsNotOpened();
pichillilorenzo's avatar
pichillilorenzo committed
299
    Map<String, dynamic> args = <String, dynamic>{};
300
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
301 302 303
    args.putIfAbsent('options', () => options);
    args.putIfAbsent('optionsType', () => "InAppBrowserOptions");
    await _ChannelManager.channel.invokeMethod('setOptions', args);
pichillilorenzo's avatar
pichillilorenzo committed
304 305
  }

pichillilorenzo's avatar
pichillilorenzo committed
306 307
  ///Gets the current [InAppBrowser] options. Returns `null` if the options are not setted yet.
  Future<Map<String, dynamic>> getOptions() async {
pichillilorenzo's avatar
pichillilorenzo committed
308
    this._throwIsNotOpened();
pichillilorenzo's avatar
pichillilorenzo committed
309
    Map<String, dynamic> args = <String, dynamic>{};
310
    args.putIfAbsent('uuid', () => uuid);
pichillilorenzo's avatar
pichillilorenzo committed
311 312 313 314
    args.putIfAbsent('optionsType', () => "InAppBrowserOptions");
    Map<dynamic, dynamic> options = await _ChannelManager.channel.invokeMethod('getOptions', args);
    options = options.cast<String, dynamic>();
    return options;
315 316
  }

pichillilorenzo's avatar
pichillilorenzo committed
317 318 319
  ///Returns `true` if the [InAppBrowser] instance is opened, otherwise `false`.
  bool isOpened() {
    return this._isOpened;
320 321
  }

pichillilorenzo's avatar
pichillilorenzo committed
322 323 324 325 326 327 328 329 330 331 332
  ///Event fires when the [InAppBrowser] starts to load an [url].
  void onLoadStart(String url) {

  }

  ///Event fires when the [InAppBrowser] finishes loading an [url].
  void onLoadStop(String url) {

  }

  ///Event fires when the [InAppBrowser] encounters an error loading an [url].
333
  void onLoadError(String url, int code, String message) {
pichillilorenzo's avatar
pichillilorenzo committed
334 335 336

  }

pichillilorenzo's avatar
pichillilorenzo committed
337 338 339 340 341
  ///Event fires when the current [progress] (range 0-100) of loading a page is changed.
  void onProgressChanged(int progress) {

  }

pichillilorenzo's avatar
pichillilorenzo committed
342 343 344 345 346
  ///Event fires when the [InAppBrowser] window is closed.
  void onExit() {

  }

347 348 349 350 351
  ///Event fires when the [InAppBrowser] webview receives a [ConsoleMessage].
  void onConsoleMessage(ConsoleMessage consoleMessage) {

  }

pichillilorenzo's avatar
pichillilorenzo committed
352
  ///Give the host application a chance to take control when a URL is about to be loaded in the current WebView.
pichillilorenzo's avatar
pichillilorenzo committed
353
  ///
pichillilorenzo's avatar
pichillilorenzo committed
354 355
  ///**NOTE**: In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`.
  void shouldOverrideUrlLoading(String url) {
pichillilorenzo's avatar
pichillilorenzo committed
356 357 358

  }

pichillilorenzo's avatar
pichillilorenzo committed
359 360 361 362 363 364
  ///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 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) {
pichillilorenzo's avatar
pichillilorenzo committed
365

pichillilorenzo's avatar
pichillilorenzo committed
366 367 368 369 370 371 372 373 374 375 376 377 378
  }

  void _throwIsAlreadyOpened({String message = ''}) {
    if (this.isOpened()) {
      throw Exception(['Error: ${ (message.isEmpty) ? '' : message + ' '}The browser is already opened.']);
    }
  }

  void _throwIsNotOpened({String message = ''}) {
    if (!this.isOpened()) {
      throw Exception(['Error: ${ (message.isEmpty) ? '' : message + ' '}The browser is not opened.']);
    }
  }
pichillilorenzo's avatar
pichillilorenzo committed
379
}
380 381 382 383 384 385 386 387

///ChromeSafariBrowser class.
///
///This class uses native [Chrome Custom Tabs](https://developer.android.com/reference/android/support/customtabs/package-summary) on Android
///and [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) on iOS.
///
///[browserFallback] represents the [InAppBrowser] instance fallback in case [Chrome Custom Tabs]/[SFSafariViewController] is not available.
class ChromeSafariBrowser {
388
  String uuid;
389
  InAppBrowser browserFallback;
pichillilorenzo's avatar
pichillilorenzo committed
390
  bool _isOpened = false;
391

pichillilorenzo's avatar
pichillilorenzo committed
392
  ///Initialize the [ChromeSafariBrowser] instance with an [InAppBrowser] fallback instance or `null`.
393
  ChromeSafariBrowser (bf) {
394
    uuid = _uuidGenerator.v4();
395 396
    browserFallback = bf;
    _ChannelManager.addListener(uuid, _handleMethod);
pichillilorenzo's avatar
pichillilorenzo committed
397
    _isOpened = false;
398 399 400 401 402 403 404 405 406 407 408 409
  }

  Future<dynamic> _handleMethod(MethodCall call) async {
    switch(call.method) {
      case "onChromeSafariBrowserOpened":
        onOpened();
        break;
      case "onChromeSafariBrowserLoaded":
        onLoaded();
        break;
      case "onChromeSafariBrowserClosed":
        onClosed();
pichillilorenzo's avatar
pichillilorenzo committed
410
        this._isOpened = false;
411
        break;
pichillilorenzo's avatar
pichillilorenzo committed
412 413
      default:
        throw UnimplementedError("Unimplemented ${call.method} method");
414 415 416
    }
  }

pichillilorenzo's avatar
pichillilorenzo committed
417
  ///Opens an [url] in a new [ChromeSafariBrowser] instance.
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
  ///
  ///- [url]: The [url] to load. Call [encodeUriComponent()] on this if the [url] contains Unicode characters.
  ///
  ///- [options]: Options for the [ChromeSafariBrowser].
  ///
  ///- [headersFallback]: The additional header of the [InAppBrowser] instance fallback to be used in the HTTP request for this URL, specified as a map from name to value.
  ///
  ///- [optionsFallback]: Options used by the [InAppBrowser] instance fallback.
  ///
  ///**Android** supports these options:
  ///
  ///- __addShareButton__: Set to `false` if you don't want the default share button. The default value is `true`.
  ///- __showTitle__: Set to `false` if the title shouldn't be shown in the custom tab. The default value is `true`.
  ///- __toolbarBackgroundColor__: Set the custom background color of the toolbar.
  ///- __enableUrlBarHiding__: Set to `true` to enable the url bar to hide as the user scrolls down on the page. The default value is `false`.
  ///- __instantAppsEnabled__: Set to `true` to enable Instant Apps. The default value is `false`.
  ///
  ///**iOS** supports these options:
436
  ///
437 438 439 440 441 442 443
  ///- __entersReaderIfAvailable__: Set to `true` if Reader mode should be entered automatically when it is available for the webpage. The default value is `false`.
  ///- __barCollapsingEnabled__: Set to `true` to enable bar collapsing. The default value is `false`.
  ///- __dismissButtonStyle__: Set the custom style for the dismiss button. The default value is `0 //done`. See [SFSafariViewController.DismissButtonStyle](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/dismissbuttonstyle) for all the available styles.
  ///- __preferredBarTintColor__: Set the custom background color of the navigation bar and the toolbar.
  ///- __preferredControlTintColor__: Set the custom color of the control buttons on the navigation bar and the toolbar.
  ///- __presentationStyle__: Set the custom modal presentation style when presenting the WebView. The default value is `0 //fullscreen`. See [UIModalPresentationStyle](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle) for all the available styles.
  ///- __transitionStyle__: Set to the custom transition style when presenting the WebView. The default value is `0 //crossDissolve`. See [UIModalTransitionStyle](https://developer.apple.com/documentation/uikit/uimodaltransitionStyle) for all the available styles.
444
  Future<void> open(String url, {Map<String, dynamic> options = const {}, Map<String, String> headersFallback = const {}, Map<String, dynamic> optionsFallback = const {}}) async {
pichillilorenzo's avatar
pichillilorenzo committed
445
    assert(url != null && url.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
446
    this._throwIsAlreadyOpened(message: 'Cannot open $url!');
447
    Map<String, dynamic> args = <String, dynamic>{};
448 449
    args.putIfAbsent('uuid', () => uuid);
    args.putIfAbsent('uuidFallback', () => (browserFallback != null) ? browserFallback.uuid : '');
450 451 452 453 454
    args.putIfAbsent('url', () => url);
    args.putIfAbsent('headers', () => headersFallback);
    args.putIfAbsent('options', () => options);
    args.putIfAbsent('optionsFallback', () => optionsFallback);
    args.putIfAbsent('useChromeSafariBrowser', () => true);
pichillilorenzo's avatar
pichillilorenzo committed
455 456
    await _ChannelManager.channel.invokeMethod('open', args);
    this._isOpened = true;
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  }

  ///Event fires when the [ChromeSafariBrowser] is opened.
  void onOpened() {

  }

  ///Event fires when the [ChromeSafariBrowser] is loaded.
  void onLoaded() {

  }

  ///Event fires when the [ChromeSafariBrowser] is closed.
  void onClosed() {

  }
pichillilorenzo's avatar
pichillilorenzo committed
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488

  bool isOpened() {
    return this._isOpened;
  }

  void _throwIsAlreadyOpened({String message = ''}) {
    if (this.isOpened()) {
      throw Exception(['Error: ${ (message.isEmpty) ? '' : message + ' '}The browser is already opened.']);
    }
  }

  void _throwIsNotOpened({String message = ''}) {
    if (!this.isOpened()) {
      throw Exception(['Error: ${ (message.isEmpty) ? '' : message + ' '}The browser is not opened.']);
    }
  }
pichillilorenzo's avatar
pichillilorenzo committed
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
}

typedef void onWebViewCreatedCallback(InAppWebViewController controller);
typedef void onWebViewLoadStartCallback(InAppWebViewController controller, String url);
typedef void onWebViewLoadStopCallback(InAppWebViewController controller, String url);
typedef void onWebViewLoadErrorCallback(InAppWebViewController controller, String url, int code, String message);
typedef void onWebViewProgressChangedCallback(InAppWebViewController controller, int progress);
typedef void onWebViewConsoleMessageCallback(InAppWebViewController controller, ConsoleMessage consoleMessage);
typedef void shouldOverrideUrlLoadingCallback(InAppWebViewController controller, String url);
typedef void onWebViewLoadResourceCallback(InAppWebViewController controller, WebResourceResponse response, WebResourceRequest request);

///InAppWebView Widget class.
///
///Flutter Widget for adding an **inline native WebView** integrated in the flutter widget tree.
///
///All platforms support these options:
///  - __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`.
///  - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`.
pichillilorenzo's avatar
pichillilorenzo committed
508
///  - __userAgent___: Set the custom WebView's user-agent.
pichillilorenzo's avatar
pichillilorenzo committed
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
///  - __javaScriptEnabled__: Set to `true` to enable JavaScript. The default value is `true`.
///  - __javaScriptCanOpenWindowsAutomatically__: Set to `true` to allow JavaScript open windows without user interaction. The default value is `false`.
///  - __mediaPlaybackRequiresUserGesture__: Set to `true` to prevent HTML5 audio or video from autoplaying. The default value is `true`.
///
///  **Android** supports these additional options:
///
///  - __clearSessionCache__: Set to `true` to have the session cookie cache cleared before the new window is opened.
///  - __builtInZoomControls__: Set to `true` if the WebView should use its built-in zoom mechanisms. The default value is `false`.
///  - __supportZoom__: Set to `false` if the WebView should not support zooming using its on-screen zoom controls and gestures. The default value is `true`.
///  - __databaseEnabled__: Set to `true` if you want the database storage API is enabled. The default value is `false`.
///  - __domStorageEnabled__: Set to `true` if you want the DOM storage API is enabled. The default value is `false`.
///  - __useWideViewPort__: Set to `true` if the WebView should enable support for the "viewport" HTML meta tag or should use a wide viewport. When the value of the setting is false, the layout width is always set to the width of the WebView control in device-independent (CSS) pixels. When the value is true and the page contains the viewport meta tag, the value of the width specified in the tag is used. If the page does not contain the tag or does not provide a width, then a wide viewport will be used. The default value is `true`.
///  - __safeBrowsingEnabled__: Set to `true` if you want the Safe Browsing is enabled. Safe Browsing allows WebView to protect against malware and phishing attacks by verifying the links. The default value is `true`.
///
///  **iOS** supports these additional options:
///
///  - __disallowOverScroll__: Set to `true` to disable the bouncing of the WebView when the scrolling has reached an edge of the content. The default value is `false`.
///  - __enableViewportScale__: Set to `true` to allow a viewport meta tag to either disable or restrict the range of user scaling. The default value is `false`.
///  - __suppressesIncrementalRendering__: Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory.. The default value is `false`.
///  - __allowsAirPlayForMediaPlayback__: Set to `true` to allow AirPlay. The default value is `true`.
///  - __allowsBackForwardNavigationGestures__: Set to `true` to allow the horizontal swipe gestures trigger back-forward list navigations. The default value is `true`.
///  - __allowsLinkPreview__: Set to `true` to allow that pressing on a link displays a preview of the destination for the link. The default value is `true`.
///  - __ignoresViewportScaleLimits__: Set to `true` if you want that the WebView should always allow scaling of the webpage, regardless of the author's intent. The ignoresViewportScaleLimits property overrides the `user-scalable` HTML property in a webpage. The default value is `false`.
///  - __allowsInlineMediaPlayback__: Set to `true` to allow HTML5 media playback to appear inline within the screen layout, using browser-supplied controls rather than native controls. For this to work, add the `webkit-playsinline` attribute to any `<video>` elements. The default value is `false`.
///  - __allowsPictureInPictureMediaPlayback__: Set to `true` to allow HTML5 videos play picture-in-picture. The default value is `true`.
class InAppWebView extends StatefulWidget {

  ///Event fires when the [InAppWebView] is created.
  final onWebViewCreatedCallback onWebViewCreated;

  ///Event fires when the [InAppWebView] starts to load an [url].
  final onWebViewLoadStartCallback onLoadStart;

  ///Event fires when the [InAppWebView] finishes loading an [url].
  final onWebViewLoadStopCallback onLoadStop;

  ///Event fires when the [InAppWebView] encounters an error loading an [url].
  final onWebViewLoadErrorCallback onLoadError;

  ///Event fires when the current [progress] of loading a page is changed.
  final onWebViewProgressChangedCallback onProgressChanged;

  ///Event fires when the [InAppWebView] receives a [ConsoleMessage].
  final onWebViewConsoleMessageCallback onConsoleMessage;

  ///Give the host application a chance to take control when a URL is about to be loaded in the current WebView.
  ///
  ///**NOTE**: In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`.
  final shouldOverrideUrlLoadingCallback shouldOverrideUrlLoading;

  ///Event fires when the [InAppWebView] loads a resource.
  ///
  ///**NOTE**: In order to be able to listen this event, you need to set `useOnLoadResource` option to `true`.
  ///
  ///**NOTE only for iOS**: In some cases, the [response.data] of a [response] with `text/assets` encoding could be empty.
  final onWebViewLoadResourceCallback onLoadResource;

  ///Initial url that will be loaded.
  final String initialUrl;
  ///Initial asset file that will be loaded. See [InAppWebView.loadFile()] for explanation.
  final String initialFile;
  ///Initial headers that will be used.
  final Map<String, String> initialHeaders;
  ///Initial options that will be used.
  final Map<String, dynamic> initialOptions;
  final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;

  const InAppWebView({
    Key key,
    this.initialUrl = "about:blank",
    this.initialFile,
    this.initialHeaders = const {},
    this.initialOptions = const {},
    this.onWebViewCreated,
    this.onLoadStart,
    this.onLoadStop,
    this.onLoadError,
    this.onConsoleMessage,
    this.onProgressChanged,
    this.shouldOverrideUrlLoading,
    this.onLoadResource,
    this.gestureRecognizers,
  }) : super(key: key);

  @override
  _InAppWebViewState createState() => _InAppWebViewState();
}

class _InAppWebViewState extends State<InAppWebView> {

  InAppWebViewController _controller;

  @override
  void dispose() {
    super.dispose();
    if (_controller != null) {
      _controller._dispose();
      _controller = null;
    }
  }

  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return GestureDetector(
        onLongPress: () {},
        child: AndroidView(
          viewType: 'com.pichillilorenzo/flutter_inappwebview',
          onPlatformViewCreated: _onPlatformViewCreated,
          gestureRecognizers: widget.gestureRecognizers,
          layoutDirection: TextDirection.rtl,
          creationParams: <String, dynamic>{
              'initialUrl': widget.initialUrl,
              'initialFile': widget.initialFile,
              'initialHeaders': widget.initialHeaders,
              'initialOptions': widget.initialOptions
            },
          creationParamsCodec: const StandardMessageCodec(),
        ),
      );
    }
    return Text(
        '$defaultTargetPlatform is not yet supported by the flutter_inappbrowser plugin');
  }

  @override
  void didUpdateWidget(InAppWebView oldWidget) {
    super.didUpdateWidget(oldWidget);
  }

  void _onPlatformViewCreated(int id) {
    _controller = InAppWebViewController(id, widget);
    if (widget.onWebViewCreated != null) {
      widget.onWebViewCreated(_controller);
    }
  }
}

pichillilorenzo's avatar
pichillilorenzo committed
647
/// Controls an [InAppWebView] widget instance.
pichillilorenzo's avatar
pichillilorenzo committed
648
///
pichillilorenzo's avatar
pichillilorenzo committed
649
/// An [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated]
pichillilorenzo's avatar
pichillilorenzo committed
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771
/// callback for an [InAppWebView] widget.
class InAppWebViewController {

  InAppWebView _widget;
  MethodChannel _channel;
  Map<String, List<JavaScriptHandlerCallback>> javaScriptHandlersMap = HashMap<String, List<JavaScriptHandlerCallback>>();
  bool _isOpened = false;
  int _id;
  String _inAppBrowserUuid;
  InAppBrowser _inAppBrowser;

  InAppWebViewController(int id, InAppWebView widget) {
    _id = id;
    _channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id');
    _channel.setMethodCallHandler(_handleMethod);
    _widget = widget;
  }

  InAppWebViewController.fromInAppBrowser(String uuid, MethodChannel channel, InAppBrowser inAppBrowser) {
    _inAppBrowserUuid = uuid;
    _channel = channel;
    _inAppBrowser = inAppBrowser;
  }

  Future<dynamic> _handleMethod(MethodCall call) async {
    switch(call.method) {
      case "onLoadStart":
        String url = call.arguments["url"];
        if (_widget != null)
          _widget.onLoadStart(this, url);
        else
          _inAppBrowser.onLoadStart(url);
        break;
      case "onLoadStop":
        String url = call.arguments["url"];
        if (_widget != null)
          _widget.onLoadStop(this, url);
        else
          _inAppBrowser.onLoadStop(url);
        break;
      case "onLoadError":
        String url = call.arguments["url"];
        int code = call.arguments["code"];
        String message = call.arguments["message"];
        if (_widget != null)
          _widget.onLoadError(this, url, code, message);
        else
          _inAppBrowser.onLoadError(url, code, message);
        break;
      case "onProgressChanged":
        int progress = call.arguments["progress"];
        if (_widget != null)
          _widget.onProgressChanged(this, progress);
        else
          _inAppBrowser.onProgressChanged(progress);
        break;
      case "shouldOverrideUrlLoading":
        String url = call.arguments["url"];
        if (_widget != null)
          _widget.shouldOverrideUrlLoading(this, url);
        else
          _inAppBrowser.shouldOverrideUrlLoading(url);
        break;
      case "onLoadResource":
        Map<dynamic, dynamic> rawResponse = call.arguments["response"];
        rawResponse = rawResponse.cast<String, dynamic>();
        Map<dynamic, dynamic> rawRequest = call.arguments["request"];
        rawRequest = rawRequest.cast<String, dynamic>();

        String urlResponse = rawResponse["url"];
        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(this, response, request);
        else
          _inAppBrowser.onLoadResource(response, request);
        break;
      case "onConsoleMessage":
        String sourceURL = call.arguments["sourceURL"];
        int lineNumber = call.arguments["lineNumber"];
        String message = call.arguments["message"];
        ConsoleMessageLevel messageLevel;
        ConsoleMessageLevel.values.forEach((element) {
          if ("ConsoleMessageLevel." + call.arguments["messageLevel"] == element.toString()) {
            messageLevel = element;
            return;
          }
        });
        if (_widget != null)
          _widget.onConsoleMessage(this, ConsoleMessage(sourceURL, lineNumber, message, messageLevel));
        else
          _inAppBrowser.onConsoleMessage(ConsoleMessage(sourceURL, lineNumber, message, messageLevel));
        break;
      case "onCallJsHandler":
        String handlerName = call.arguments["handlerName"];
        List<dynamic> args = jsonDecode(call.arguments["args"]);
        if (javaScriptHandlersMap.containsKey(handlerName)) {
          for (var handler in javaScriptHandlersMap[handlerName]) {
            handler(args);
          }
        }
        break;
      default:
        throw UnimplementedError("Unimplemented ${call.method} method");
    }
  }

  ///Loads the given [url] with optional [headers] specified as a map from name to value.
  Future<void> loadUrl(String url, {Map<String, String> headers = const {}}) async {
pichillilorenzo's avatar
pichillilorenzo committed
772
    assert(url != null && url.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
773 774 775 776 777 778 779 780 781 782
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened(message: 'Cannot laod $url!');
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('url', () => url);
    args.putIfAbsent('headers', () => headers);
    await _channel.invokeMethod('loadUrl', args);
  }

pichillilorenzo's avatar
pichillilorenzo committed
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
  ///Loads the given [url] with [postData] using `POST` method into this WebView.
  Future<void> postUrl(String url, Uint8List postData) async {
    assert(url != null && url.isNotEmpty);
    assert(postData != null);
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened(message: 'Cannot laod $url!');
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('url', () => url);
    args.putIfAbsent('postData', () => postData);
    await _channel.invokeMethod('postUrl', args);
  }

  ///Loads the given [data] into this WebView, using [baseUrl] as the base URL for the content.
  ///The [mimeType] parameter specifies the format of the data.
  ///The [encoding] parameter specifies the encoding of the data.
  Future<void> loadData(String data, {String mimeType = "text/html", String encoding = "utf8", String baseUrl = "about:blank"}) async {
    assert(data != null);
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('data', () => data);
    args.putIfAbsent('mimeType', () => mimeType);
    args.putIfAbsent('encoding', () => encoding);
    args.putIfAbsent('baseUrl', () => baseUrl);
    await _channel.invokeMethod('loadData', args);
  }

pichillilorenzo's avatar
pichillilorenzo committed
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843
  ///Loads the given [assetFilePath] with optional [headers] specified as a map from name to value.
  ///
  ///To be able to load your local files (assets, js, css, etc.), you need to add them in the `assets` section of the `pubspec.yaml` file, otherwise they cannot be found!
  ///
  ///Example of a `pubspec.yaml` file:
  ///```yaml
  ///...
  ///
  ///# The following section is specific to Flutter.
  ///flutter:
  ///
  ///  # The following line ensures that the Material Icons font is
  ///  # included with your application, so that you can use the icons in
  ///  # the material Icons class.
  ///  uses-material-design: true
  ///
  ///  assets:
  ///    - assets/index.html
  ///    - assets/css/
  ///    - assets/images/
  ///
  ///...
  ///```
  ///Example of a `main.dart` file:
  ///```dart
  ///...
  ///inAppBrowser.loadFile("assets/index.html");
  ///...
  ///```
  Future<void> loadFile(String assetFilePath, {Map<String, String> headers = const {}}) async {
pichillilorenzo's avatar
pichillilorenzo committed
844
    assert(assetFilePath != null && assetFilePath.isNotEmpty);
pichillilorenzo's avatar
pichillilorenzo committed
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened(message: 'Cannot laod $assetFilePath!');
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('url', () => assetFilePath);
    args.putIfAbsent('headers', () => headers);
    await _channel.invokeMethod('loadFile', args);
  }

  ///Reloads the [InAppWebView] window.
  Future<void> reload() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    await _channel.invokeMethod('reload', args);
  }

  ///Goes back in the history of the [InAppWebView] window.
  Future<void> goBack() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    await _channel.invokeMethod('goBack', args);
  }

  ///Returns a Boolean value indicating whether the [InAppWebView] can move backward.
  Future<bool> canGoBack() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    return await _channel.invokeMethod('canGoBack', args);
  }

  ///Goes forward in the history of the [InAppWebView] window.
  Future<void> goForward() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    await _channel.invokeMethod('goForward', args);
  }

  ///Returns a Boolean value indicating whether the [InAppWebView] can move forward.
  Future<bool> canGoForward() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    return await _channel.invokeMethod('canGoForward', args);
  }

  ///Check if the Web View of the [InAppWebView] instance is in a loading state.
  Future<bool> isLoading() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    return await _channel.invokeMethod('isLoading', args);
  }

  ///Stops the Web View of the [InAppWebView] instance from loading.
  Future<void> stopLoading() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    await _channel.invokeMethod('stopLoading', args);
  }

  ///Injects JavaScript code into the [InAppWebView] window and returns the result of the evaluation.
  Future<String> injectScriptCode(String source) async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('source', () => source);
    return await _channel.invokeMethod('injectScriptCode', args);
  }

  ///Injects a JavaScript file into the [InAppWebView] window.
  Future<void> injectScriptFile(String urlFile) async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('urlFile', () => urlFile);
    await _channel.invokeMethod('injectScriptFile', args);
  }

  ///Injects CSS into the [InAppWebView] window.
  Future<void> injectStyleCode(String source) async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('source', () => source);
    await _channel.invokeMethod('injectStyleCode', args);
  }

  ///Injects a CSS file into the [InAppWebView] window.
  Future<void> injectStyleFile(String urlFile) async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('urlFile', () => urlFile);
    await _channel.invokeMethod('injectStyleFile', args);
  }

  ///Adds/Appends a JavaScript message handler [callback] ([JavaScriptHandlerCallback]) that listen to post messages sent from JavaScript by the handler with name [handlerName].
  ///Returns the position `index` of the handler that can be used to remove it with the [removeJavaScriptHandler()] method.
  ///
  ///The Android implementation uses [addJavascriptInterface](https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface(java.lang.Object,%20java.lang.String)).
  ///The iOS implementation uses [addScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537172-addscriptmessagehandler?language=objc)
  ///
  ///The JavaScript function that can be used to call the handler is `window.flutter_inappbrowser.callHandler(handlerName <String>, ...args);`, where `args` are [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters).
  ///The `args` will be stringified automatically using `JSON.stringify(args)` method and then they will be decoded on the Dart side.
  int addJavaScriptHandler(String handlerName, JavaScriptHandlerCallback callback) {
    this.javaScriptHandlersMap.putIfAbsent(handlerName, () => List<JavaScriptHandlerCallback>());
    this.javaScriptHandlersMap[handlerName].add(callback);
    return this.javaScriptHandlersMap[handlerName].indexOf(callback);
  }

  ///Removes a JavaScript message handler previously added with the [addJavaScriptHandler()] method in the [handlerName] list by its position [index].
  ///Returns `true` if the callback is removed, otherwise `false`.
  bool removeJavaScriptHandler(String handlerName, int index) {
    try {
      this.javaScriptHandlersMap[handlerName].removeAt(index);
      return true;
    }
    on RangeError catch(e) {
      print(e);
    }
    return false;
  }

  ///Takes a screenshot (in PNG format) of the WebView's visible viewport and returns a `Uint8List`. Returns `null` if it wasn't be able to take it.
  ///
  ///**NOTE for iOS**: available from iOS 11.0+.
  Future<Uint8List> takeScreenshot() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    return await _channel.invokeMethod('takeScreenshot', args);
  }

  ///Sets the [InAppWebView] options with the new [options] and evaluates them.
  Future<void> setOptions(Map<String, dynamic> options) async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('options', () => options);
    args.putIfAbsent('optionsType', () => "InAppBrowserOptions");
    await _channel.invokeMethod('setOptions', args);
  }

  ///Gets the current [InAppWebView] options. Returns `null` if the options are not setted yet.
  Future<Map<String, dynamic>> getOptions() async {
    Map<String, dynamic> args = <String, dynamic>{};
    if (_inAppBrowserUuid != null) {
      _inAppBrowser._throwIsNotOpened();
      args.putIfAbsent('uuid', () => _inAppBrowserUuid);
    }
    args.putIfAbsent('optionsType', () => "InAppBrowserOptions");
    Map<dynamic, dynamic> options = await _ChannelManager.channel.invokeMethod('getOptions', args);
    options = options.cast<String, dynamic>();
    return options;
  }

  Future<void> _dispose() async {
    await _channel.invokeMethod('dispose');
  }

}

///InAppLocalhostServer class.
///
///This class allows you to create a simple server on `http://localhost:[port]/` in order to be able to load your assets file on a server. The default [port] value is `8080`.
class InAppLocalhostServer {

  HttpServer _server;
  int _port = 8080;

  InAppLocalhostServer({int port = 8080}) {
    this._port = port;
  }

  ///Starts a server on http://localhost:[port]/.
  ///
  ///**NOTE for iOS**: For the iOS Platform, you need to add the `NSAllowsLocalNetworking` key with `true` in the `Info.plist` file (See [ATS Configuration Basics](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW35)):
  ///```xml
  ///<key>NSAppTransportSecurity</key>
  ///<dict>
  ///    <key>NSAllowsLocalNetworking</key>
  ///    <true/>
  ///</dict>
  ///```
  ///The `NSAllowsLocalNetworking` key is available since **iOS 10**.
  Future<void> start() async {

    if (this._server != null) {
      throw Exception('Server already started on http://localhost:$_port');
    }

    var completer = new Completer();

    runZoned(() {
      HttpServer.bind('127.0.0.1', _port).then((server) {
        print('Server running on http://localhost:' + _port.toString());

        this._server = server;

        server.listen((HttpRequest request) async {
          var body = List<int>();
          var path = request.requestedUri.path;
          path = (path.startsWith('/')) ? path.substring(1) : path;
          path += (path.endsWith('/')) ? 'index.html' : '';

          try {
            body = (await rootBundle.load(path))
                .buffer.asUint8List();
          } catch (e) {
            print(e.toString());
            request.response.close();
            return;
          }

          var contentType = ['text', 'html'];
          if (!request.requestedUri.path.endsWith('/') && request.requestedUri.pathSegments.isNotEmpty) {
            var mimeType = lookupMimeType(request.requestedUri.path, headerBytes: body);
            if (mimeType != null) {
              contentType = mimeType.split('/');
            }
          }

          request.response.headers.contentType = new ContentType(contentType[0], contentType[1], charset: 'utf-8');
          request.response.add(body);
          request.response.close();
        });

        completer.complete();
      });
    }, onError: (e, stackTrace) => print('Error: $e $stackTrace'));

    return completer.future;
  }

  ///Closes the server.
  Future<void> close() async {
    if (this._server != null) {
      await this._server.close(force: true);
      print('Server running on http://localhost:$_port closed');
      this._server = null;
    }
  }

1120 1121
}

1122
///Manages the cookies used by an application's [InAppWebView] instances.
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
class CookieManager {
  static bool _initialized = false;
  static const MethodChannel _channel = const MethodChannel('com.pichillilorenzo/flutter_inappbrowser_cookiemanager');

  static void _init () {
    _channel.setMethodCallHandler(_handleMethod);
    _initialized = true;
  }

  static Future<dynamic> _handleMethod(MethodCall call) async {
  }

1135
  ///Sets a cookie for the given [url]. Any existing cookie with the same [host], [path] and [name] will be replaced with the new cookie. The cookie being set will be ignored if it is expired.
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161
  static Future<void> setCookie(String url, String name, String value, String domain,
      { String path = "/",
        int expiresDate,
        bool isHTTPOnly,
        bool isSecure }) async {
    if (!_initialized)
      _init();

    assert(url != null && url.isNotEmpty);
    assert(name != null && name.isNotEmpty);
    assert(value != null && value.isNotEmpty);
    assert(domain != null && domain.isNotEmpty);
    assert(path != null && path.isNotEmpty);

    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('url', () => url);
    args.putIfAbsent('name', () => name);
    args.putIfAbsent('value', () => value);
    args.putIfAbsent('domain', () => domain);
    args.putIfAbsent('path', () => path);
    args.putIfAbsent('expiresDate', () => expiresDate);
    args.putIfAbsent('isHTTPOnly', () => isHTTPOnly);
    args.putIfAbsent('isSecure', () => isSecure);

    await _channel.invokeMethod('setCookie', args);
  }
1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193

  ///Gets all the cookies for the given [url].
  static Future<List<Map<String, dynamic>>> getCookies(String url) async {
    assert(url != null && url.isNotEmpty);

    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('url', () => url);
    List<dynamic> cookies = await _channel.invokeMethod('getCookies', args);
    cookies = cookies.cast<Map<dynamic, dynamic>>();
    for(var i = 0; i < cookies.length; i++) {
      cookies[i] = cookies[i].cast<String, dynamic>();
    }
    cookies = cookies.cast<Map<String, dynamic>>();
    return cookies;
  }

  ///Gets a cookie by its [cookieName] for the given [url].
  static Future<Map<String, dynamic>> getCookie(String url, String cookieName) async {
    assert(url != null && url.isNotEmpty);
    assert(cookieName != null && cookieName.isNotEmpty);

    Map<String, dynamic> args = <String, dynamic>{};
    args.putIfAbsent('url', () => url);
    List<dynamic> cookies = await _channel.invokeMethod('getCookies', args);
    cookies = cookies.cast<Map<dynamic, dynamic>>();
    for(var i = 0; i < cookies.length; i++) {
      cookies[i] = cookies[i].cast<String, dynamic>();
      if (cookies[i]["name"] == cookieName)
        return cookies[i];
    }
    return null;
  }
1194
}