diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml new file mode 100644 index 00000000..3d5970d9 --- /dev/null +++ b/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,772 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index c241dc8c..31799730 100755 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,9 +1,6 @@ - - - - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c5ddc6..9a89838d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ - Added `allowUniversalAccessFromFileURLs` and `allowFileAccessFromFileURLs` WebView options also for iOS (also thanks to [liranhao](https://github.com/liranhao)) - Added limited cookies support on iOS below 11.0 using JavaScript - Added `IOSCookieManager` class and `CookieManager.instance().ios.getAllCookies` iOS-specific method -- Added `UserScript`, `UserScriptInjectionTime`, `ContentWorld`, `AndroidWebViewFeature`, `AndroidServiceWorkerController`, `AndroidServiceWorkerClient`, `ScreenshotConfiguration`, `IOSWKPDFConfiguration` classes +- Added `UserScript`, `UserScriptInjectionTime`, `ContentWorld`, `AndroidWebViewFeature`, `AndroidServiceWorkerController`, `AndroidServiceWorkerClient`, `ScreenshotConfiguration`, `IOSWKPDFConfiguration`, `URLRequest` classes - Added `initialUserScripts` WebView option -- Added `addUserScript`, `addUserScripts`, `removeUserScript`, `removeUserScripts`, `removeAllUserScripts`, `callAsyncJavaScript` WebView methods +- Added `addUserScript`, `addUserScripts`, `removeUserScript`, `removeUserScripts`, `removeUserScriptsByGroupName`, `removeAllUserScripts`, `callAsyncJavaScript`, `isSecureContext` WebView methods - Added `contentWorld` argument to `evaluateJavascript` WebView method -- Added `isDirectionalLockEnabled`, `mediaType`, `pageZoom`, `limitsNavigationsToAppBoundDomains`, `useOnNavigationResponse`, `applePayAPIEnabled`, `allowingReadAccessTo` iOS-specific WebView options +- Added `isDirectionalLockEnabled`, `mediaType`, `pageZoom`, `limitsNavigationsToAppBoundDomains`, `useOnNavigationResponse`, `applePayAPIEnabled`, `allowingReadAccessTo`, `disableLongPressContextMenuOnLinks` iOS-specific WebView options - Added `handlesURLScheme`, `createPdf`, `createWebArchiveData` iOS-specific WebView methods - Added `iosOnNavigationResponse` and `iosShouldAllowDeprecatedTLS` iOS-specific WebView events - Added `iosAnimated` optional argument to `zoomBy` WebView method @@ -18,6 +18,7 @@ - Added `cssLinkHtmlTagAttributes` optional argument to `injectCSSFileFromUrl` WebView method - Added `iosAllowingReadAccessTo` iOS-specific optional argument to `loadUrl` WebView method - Added new iOS-specific attributes to `ShouldOverrideUrlLoadingRequest` and `CreateWindowRequest` classes +- Added `toolbarTopTranslucent`, `toolbarTopTintColor`, `toolbarBottomTintColor`, `toolbarTopBarTintColor` ios-specific InAppBrowser options - Updated integration tests - Merge "Upgraded appcompat to 1.2.0-rc-02" [#465](https://github.com/pichillilorenzo/flutter_inappwebview/pull/465) (thanks to [andreidiaconu](https://github.com/andreidiaconu)) - Merge "Added missing field 'headers' which returned by WebResourceResponse.toMap()" [#490](https://github.com/pichillilorenzo/flutter_inappwebview/pull/490) (thanks to [Doflatango](https://github.com/Doflatango)) @@ -47,14 +48,44 @@ - Minimum Flutter version required is `1.22.2` and Dart SDK `>=2.12.0-0 <3.0.0` - iOS Xcode version `>= 12` -- Removed `debuggingEnabled` WebView option; on Android you should use now the `AndroidInAppWebViewController.setWebContentsDebuggingEnabled(bool debuggingEnabled)` static method; on iOS, debugging is always enabled - `allowUniversalAccessFromFileURLs` and `allowFileAccessFromFileURLs` WebView options moved from Android-specific options to cross-platform options - Added `callAsyncJavaScript` name to the list of javaScriptHandlerForbiddenNames -- Changed `zoomBy` WebView method signature - Moved `saveWebArchive` WebView method from Android-specific to cross-platform +- Moved `progressBar` InAppBroswer from Android-specific option to cross-platform option and renamed to `hideProgressBar` - Renamed `HttpAuthChallenge` to `URLAuthenticationChallenge` -- Deleted `androidOnRequestFocus` event because it is never called - Updated `basicConstraints`, `subjectKeyIdentifier`, `authorityKeyIdentifier`, `certificatePolicies`, `cRLDistributionPoints`, `authorityInfoAccess` attributes type of `X509Certificate` +- Updated "WebView.storyboard" for InAppBrowser iOS representation +- Renamed `ShouldOverrideUrlLoadingAction` class to `NavigationActionPolicy` +- Renamed `ProtectionSpace` class to `URLProtectionSpace` +- Renamed `ProtectionSpaceHttpAuthCredentials` to `URLProtectionSpaceHttpAuthCredentials` +- Renamed `CreateWindowRequest` class to `CreateWindowAction` +- Renamed `initialUrl` to `initialUrlRequest` WebView attribute and made it of type `URLRequest` +- Renamed `toolbarTop` InAppBrowser cross-platform option to `hideToolbarTop` +- Renamed `toolbarBottom` InAppBrowser ios-specific option to `hideToolbarBottom` +- Removed `debuggingEnabled` WebView option; on Android you should use now the `AndroidInAppWebViewController.setWebContentsDebuggingEnabled(bool debuggingEnabled)` static method; on iOS, debugging is always enabled +- Removed `androidOnRequestFocus` event because it is never called +- Removed `initialHeaders` WebView attribute. Use `URLRequest.headers` attribute +- Removed `headers` argument from `loadFile` WebView method +- Removed `headers` argument from `openFile` InAppBrowser method +- Removed `headers` argument from `loadUrl` WebView method, renamed the `url` argument to `urlRequest` and made it of type `URLRequest` +- Removed `headers` argument from `openFile` InAppBrowser method +- Removed `headers` argument from `openUrl` InAppBrowser method, renamed the `url` argument to `urlRequest` and made it of type `URLRequest` +- Removed `fallback` argument from `ChromeSafariBrowser` constructor. Check for availability of `ChromeSafariBrowser` if you want show one or the other. +- Removed `scheme` argument from `onLoadResourceCustomScheme` WebView event. Use the `Uri url` parameter now. +- Removed `ShouldOverrideUrlLoadingRequest` class and replaced with `NavigationAction` +- Changed `zoomBy` WebView method signature +- Changed type of `urlFile` argument of `injectCSSFileFromUrl` WebView method to `Uri` +- Changed type of `urlFile` argument of `injectJavascriptFileFromUrl` WebView method to `Uri` +- Changed return type of `getOriginalUrl` Android-specific WebView method to `Uri` +- Changed return type of `getSafeBrowsingPrivacyPolicyUrl` Android-specific WebView method to `Uri` +- Changed type of `url` argument of `onLoadStart`, `onLoadStop`, `onLoadError`, `onLoadHttpError`, `onLoadResourceCustomScheme`, `onUpdateVisitedHistory`, `onPrint`, `onPageCommitVisible`, `androidOnSafeBrowsingHit`, `androidOnRenderProcessUnresponsive`, `androidOnRenderProcessResponsive`, `androidOnFormResubmission`, `androidOnReceivedTouchIconUrl` WebView events to `Uri` +- Changed type of `baseUrl` and `androidHistoryUrl` arguments of `loadData` WebView method and `openData` InAppBrowser method +- Changed `openUrl` InAppBrowser method to `openUrlRequest` +- Changed type of `url` argument of `openWithSystemBrowser` InAppBrowser method to `Uri` +- Changed all InAppBrowser color options type from `String` to `Color` +- Changed all ChromeSafariBrowser color options type from `String` to `Color` +- Updated attributes of `ShouldOverrideUrlLoadingRequest`, `ServerTrustChallenge` and `ClientCertChallenge` classes +- Changed type of `url` attribute to `Uri` for `JsAlertRequest`, `JsAlertConfirm`, `JsPromptRequest` classes ## 4.0.0+4 diff --git a/README.md b/README.md index 1df4dd1e..2866c9ab 100755 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ Also, check the [example/integration_test/webview_flutter_test.dart](https://git ## Articles/Resources -- [InAppWebView: The Real Power of WebViews in Flutter](https://medium.com/flutter-community/inappwebview-the-real-power-of-webviews-in-flutter-c6d52374209d?source=friends_link&sk=cb74487219bcd85e610a670ee0b447d0) -- [Creating a Full-Featured Browser using WebViews in Flutter](https://medium.com/flutter-community/creating-a-full-featured-browser-using-webviews-in-flutter-9c8f2923c574?source=friends_link&sk=55fc8267f351082aa9e73ced546f6bcb) +- [InAppWebView: The Real Power of WebViews in Flutter](https://medium.com/flutter-community/inappwebview-the-real-power-of-webviews-in-flutter-c6d52374209d?source=friends_link&sk=cb74487219bcd85e610a670ee0b447d0) (valid for plugin version 4.0.0) +- [Creating a Full-Featured Browser using WebViews in Flutter](https://medium.com/flutter-community/creating-a-full-featured-browser-using-webviews-in-flutter-9c8f2923c574?source=friends_link&sk=55fc8267f351082aa9e73ced546f6bcb) (valid for plugin version 4.0.0) - [Flutter Browser App: A Full-Featured Mobile Browser App (such as the Google Chrome mobile browser) created using Flutter and the features offered by the flutter_inappwebview plugin](https://github.com/pichillilorenzo/flutter_browser_app) ## Requirements @@ -323,24 +323,31 @@ class _MyAppState extends State { decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)), child: InAppWebView( - initialUrl: "https://flutter.dev/", - initialHeaders: {}, + initialUrlRequest: URLRequest( + url: Uri.parse("https://flutter.dev/") + ), initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( + crossPlatform: InAppWebViewOptions( - ) + ), + ios: IOSInAppWebViewOptions( + + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true + ) ), onWebViewCreated: (InAppWebViewController controller) { webView = controller; }, onLoadStart: (controller, url) { setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onLoadStop: (controller, url) async { setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onProgressChanged: (controller, progress) { @@ -354,19 +361,19 @@ class _MyAppState extends State { ButtonBar( alignment: MainAxisAlignment.center, children: [ - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_back), onPressed: () { webView?.goBack(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_forward), onPressed: () { webView?.goForward(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.refresh), onPressed: () { webView?.reload(); @@ -395,8 +402,8 @@ Screenshots: ##### `InAppWebViewController` Cross-platform methods * `addJavaScriptHandler({required String handlerName, required JavaScriptHandlerCallback callback})`: Adds a JavaScript message handler callback that listen to post messages sent from JavaScript by the handler with name `handlerName`. -* `addUserScript(UserScript userScript)`: Injects the specified `userScript` into the webpage’s content. -* `addUserScripts(List userScripts)`: Injects the `userScripts` into the webpage’s content. +* `addUserScript({required UserScript userScript})`: Injects the specified `userScript` into the webpage’s content. +* `addUserScripts({required List userScripts})`: Injects the `userScripts` into the webpage’s content. * `callAsyncJavaScript({required String functionBody, Map arguments = const {}, ContentWorld? contentWorld})`: Executes the specified string as an asynchronous JavaScript function. * `canGoBackOrForward({required int steps})`: Returns a boolean value indicating whether the WebView can go back or forward the given number of steps. Steps is negative if backward and positive if forward. * `canGoBack`: Returns a boolean value indicating whether the WebView can move backward. @@ -433,26 +440,28 @@ Screenshots: * `injectCSSFileFromAsset({required String assetFilePath})`: Injects a CSS file into the WebView from the flutter assets directory. * `injectCSSFileFromUrl({required String urlFile, CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes})`: Injects an external CSS file into the WebView from a defined url. * `injectJavascriptFileFromAsset({required String assetFilePath})`: Injects a JavaScript file into the WebView from the flutter assets directory. -* `injectJavascriptFileFromUrl({required String urlFile, ScriptHtmlTagAttributes? scriptHtmlTagAttributes})`: Injects an external JavaScript file into the WebView from a defined url. +* `injectJavascriptFileFromUrl({required Uri urlFile, ScriptHtmlTagAttributes? scriptHtmlTagAttributes})`: Injects an external JavaScript file into the WebView from a defined url. * `isLoading`: Check if the WebView instance is in a loading state. -* `loadData({required String data, String mimeType = "text/html", String encoding = "utf8", String baseUrl = "about:blank", String androidHistoryUrl = "about:blank"})`: Loads the given data into this WebView. -* `loadFile({required String assetFilePath, Map headers = const {}})`: Loads the given `assetFilePath` with optional headers specified as a map from name to value. -* `loadUrl({required String url, Map headers = const {}, String? iosAllowingReadAccessTo})`: Loads the given url with optional headers specified as a map from name to value. +* `isSecureContext`: Indicates whether the webpage context is capable of using features that require secure contexts. +* `loadData({required String data, String mimeType = "text/html", String encoding = "utf8", Uri? baseUrl, Uri? androidHistoryUrl})`: Loads the given data into this WebView. +* `loadFile({required String assetFilePath})`: Loads the given `assetFilePath` with optional headers specified as a map from name to value. +* `loadUrl({required URLRequest urlRequest, Uri? iosAllowingReadAccessTo})`: Loads the given url with optional headers specified as a map from name to value. * `pauseTimers`: On Android, it pauses all layout, parsing, and JavaScript timers for all WebViews. This is a global requests, not restricted to just this WebView. This can be useful if the application has been paused. On iOS, it is restricted to just this WebView. -* `postUrl({required String url, required Uint8List postData})`: Loads the given url with postData using `POST` method into this WebView. +* `postUrl({required Uri url, required Uint8List postData})`: Loads the given url with postData using `POST` method into this WebView. * `printCurrentPage`: Prints the current page. * `reload`: Reloads the WebView. * `removeAllUserScripts()`: Removes all the user scripts from the webpage’s content. * `removeJavaScriptHandler({required String handlerName})`: Removes a JavaScript message handler previously added with the `addJavaScriptHandler()` associated to `handlerName` key. -* `removeUserScript(UserScript userScript)`: Removes the specified `userScript` from the webpage’s content. -* `removeUserScripts(List userScripts)`: Removes the `userScripts` from the webpage’s content. +* `removeUserScript({required UserScript userScript})`: Removes the specified `userScript` from the webpage’s content. +* `removeUserScriptsByGroupName({required String groupName})`: Removes all the `UserScript`s with `groupName` as group name from the webpage’s content. +* `removeUserScripts({required List userScripts})`: Removes the `userScripts` from the webpage’s content. * `requestFocusNodeHref`: Requests the anchor or image element URL at the last tapped point. * `requestImageRef`: Requests the URL of the image last touched by the user. * `resumeTimers`: On Android, it resumes all layout, parsing, and JavaScript timers for all WebViews. This will resume dispatching all timers. On iOS, it resumes all layout, parsing, and JavaScript timers to just this WebView. * `saveWebArchive({required String filePath, bool autoname = false})`: Saves the current view as a web archive. * `scrollBy({required int x, required int y, bool animated = false})`: Moves the scrolled position of the WebView. * `scrollTo({required int x, required int y, bool animated = false})`: Scrolls the WebView to the position. -* `setContextMenu(ContextMenu contextMenu)`: Sets or updates the WebView context menu to be used next time it will appear. +* `setContextMenu(ContextMenu? contextMenu)`: Sets or updates the WebView context menu to be used next time it will appear. * `setOptions({required InAppWebViewGroupOptions options})`: Sets the WebView options with the new options and evaluates them. * `stopLoading`: Stops the WebView from loading. * `takeScreenshot({ScreenshotConfiguration? screenshotConfiguration})`: 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. @@ -611,6 +620,7 @@ Instead, on the `onLoadStop` WebView event, you can use `callHandler` directly: * `minimumLogicalFontSize`: Sets the minimum logical font size. The default is `8`. * `mixedContentMode`: Configures the WebView's behavior when a secure origin attempts to load a resource from an insecure origin. * `needInitialFocus`: Tells the WebView whether it needs to set a node. The default value is `true`. +* `networkAvailable`: Informs WebView of the network state. * `offscreenPreRaster`: Sets whether this WebView should raster tiles when it is offscreen but attached to a window. * `overScrollMode`: Sets the WebView's over-scroll mode. The default value is `AndroidOverScrollMode.OVER_SCROLL_IF_CONTENT_SCROLLS`. * `regexToCancelSubFramesLoading`: Regular expression used by `shouldOverrideUrlLoading` event to cancel navigation for frames that are not the main frame. If the url request of a subframe matches the regular expression, then the request of that subframe is canceled. @@ -649,6 +659,7 @@ Instead, on the `onLoadStop` WebView event, you can use `callHandler` directly: * `contentInsetAdjustmentBehavior`: Configures how safe area insets are added to the adjusted content inset. The default value is `IOSUIScrollViewContentInsetAdjustmentBehavior.NEVER`. * `dataDetectorTypes`: Specifying a dataDetectoryTypes value adds interactivity to web content that matches the value. * `decelerationRate`: A `IOSUIScrollViewDecelerationRate` value that determines the rate of deceleration after the user lifts their finger. The default value is `IOSUIScrollViewDecelerationRate.NORMAL`. +* `disableLongPressContextMenuOnLinks`: Set to `true` to disable the context menu (copy, select, etc.) that is shown when the user emits a long press event on a HTML link. * `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`. * `ignoresViewportScaleLimits`: Set to `true` if you want that the WebView should always allow scaling of the webpage, regardless of the author's intent. @@ -664,7 +675,7 @@ Instead, on the `onLoadStop` WebView event, you can use `callHandler` directly: * `selectionGranularity`: The level of granularity with which the user can interactively select content in the web view. * `sharedCookiesEnabled`: Set `true` if shared cookies from `HTTPCookieStorage.shared` should used for every load request in the WebView. * `suppressesIncrementalRendering`: Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory. The default value is `false`. -* `useOnNavigationResponse`: Set to `true` to be able to listen at the `iosOnNavigationResponse` event. The default value is `false`. +* `useOnNavigationResponse`: Set to `true` to be able to listen to the `iosOnNavigationResponse` event. The default value is `false`. #### `InAppWebView` Events @@ -814,25 +825,32 @@ class _MyAppState extends State { decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)), child: InAppWebView( - initialUrl: "https://flutter.dev/", + initialUrlRequest: URLRequest( + url: Uri.parse("https://flutter.dev/") + ), contextMenu: contextMenu, - initialHeaders: {}, initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( + crossPlatform: InAppWebViewOptions( - ) + ), + ios: IOSInAppWebViewOptions( + + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true + ) ), onWebViewCreated: (InAppWebViewController controller) { webView = controller; }, onLoadStart: (controller, url) { setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onLoadStop: (controller, url) async { setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onProgressChanged: (controller, progress) { @@ -846,19 +864,19 @@ class _MyAppState extends State { ButtonBar( alignment: MainAxisAlignment.center, children: [ - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_back), onPressed: () { webView?.goBack(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_forward), onPressed: () { webView?.goForward(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.refresh), onPressed: () { webView?.reload(); @@ -922,7 +940,9 @@ class _MyAppState extends State { super.initState(); headlessWebView = new HeadlessInAppWebView( - initialUrl: "https://flutter.dev/", + initialUrlRequest: URLRequest( + url: Uri.parse("https://flutter.dev/") + ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( @@ -937,19 +957,19 @@ class _MyAppState extends State { onLoadStart: (controller, url) async { print("onLoadStart $url"); setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onLoadStop: (controller, url) async { print("onLoadStop $url"); setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, onUpdateVisitedHistory: (controller, url, androidIsReload) { print("onUpdateVisitedHistory $url"); setState(() { - this.url = url ?? ''; + this.url = url?.toString() ?? ''; }); }, ); @@ -976,7 +996,7 @@ class _MyAppState extends State { "CURRENT URL\n${(url.length > 50) ? url.substring(0, 50) + "..." : url}"), ), Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () async { await headlessWebView?.dispose(); await headlessWebView?.run(); @@ -984,7 +1004,7 @@ class _MyAppState extends State { child: Text("Run HeadlessInAppWebView")), ), Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () async { try { await headlessWebView?.webViewController.evaluateJavascript(source: """console.log('Here is the message!');"""); @@ -995,7 +1015,7 @@ class _MyAppState extends State { child: Text("Send console.log message")), ), Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () { headlessWebView?.dispose(); }, @@ -1054,9 +1074,9 @@ class MyInAppBrowser extends InAppBrowser { } @override - Future shouldOverrideUrlLoading(ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest) async { - print("\n\n override ${shouldOverrideUrlLoadingRequest.url}\n\n"); - return ShouldOverrideUrlLoadingAction.ALLOW; + Future? shouldOverrideUrlLoading(NavigationAction navigationAction) async { + print("\n\n override ${navigationAction.request.url}\n\n"); + return NavigationActionPolicy.ALLOW; } @override @@ -1066,7 +1086,7 @@ class MyInAppBrowser extends InAppBrowser { "ms ---> duration: " + response.duration.toString() + "ms " + - (response.url ?? '')); + (response.url?.toString() ?? '')); } @override @@ -1074,7 +1094,7 @@ class MyInAppBrowser extends InAppBrowser { print(""" console output: message: ${consoleMessage.message} - messageLevel: ${consoleMessage.messageLevel?.toValue()} + messageLevel: ${consoleMessage.messageLevel.toValue()} """); } } @@ -1106,7 +1126,7 @@ class _MyAppState extends State { title: const Text('InAppBrowser Example'), ), body: Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () { widget.browser.openFile( assetFilePath: "assets/index.html", @@ -1136,10 +1156,10 @@ Screenshots: #### `InAppBrowser` Methods -* `open({String url = "about:blank", Map headers = const {}, InAppBrowserClassOptions options})`: Opens an `url` in a new `InAppBrowser` instance. -* `openFile({required String assetFilePath, Map headers = const {}, InAppBrowserClassOptions options})`: Opens the given `assetFilePath` file in a new `InAppBrowser` instance. The other arguments are the same of `InAppBrowser.open`. -* `openData({required String data, String mimeType = "text/html", String encoding = "utf8", String baseUrl = "about:blank", String historyUrl = "about:blank", InAppBrowserClassOptions options})`: Opens a new `InAppBrowser` instance with `data` as a content, using `baseUrl` as the base URL for it. -* `openWithSystemBrowser({required String url})`: This is a static method that opens an `url` in the system browser. You wont be able to use the `InAppBrowser` methods here! +* `openUrlRequest({required URLRequest urlRequest, InAppBrowserClassOptions? options})`: Opens an `url` in a new `InAppBrowser` instance. +* `openFile({required String assetFilePath, InAppBrowserClassOptions? options})`: Opens the given `assetFilePath` file in a new `InAppBrowser` instance. The other arguments are the same of `InAppBrowser.open`. +* `openData({required String data, String mimeType = "text/html", String encoding = "utf8", Uri? baseUrl, Uri? androidHistoryUrl, InAppBrowserClassOptions? options})`: Opens a new `InAppBrowser` instance with `data` as a content, using `baseUrl` as the base URL for it. +* `openWithSystemBrowser({required Uri url})`: This is a static method that opens an `url` in the system browser. You wont be able to use the `InAppBrowser` methods here! * `show`: Displays an `InAppBrowser` window that was opened hidden. Calling this has no effect if the `InAppBrowser` was already visible. * `hide`: Hides the `InAppBrowser` window. Calling this has no effect if the `InAppBrowser` was already hidden. * `close`: Closes the `InAppBrowser` window. @@ -1157,25 +1177,28 @@ Specific options of the `InAppBrowser` class are: * `hidden`: Set to `true` to create the browser and load the page, but not show it. Omit or set to `false` to have the browser open and load normally. The default value is `false`. * `hideUrlBar`: Set to `true` to hide the url bar on the toolbar at the top. The default value is `false`. +* `hideProgressBar`: Set to `true` to hide the progress bar when the WebView is loading a page. The default value is `false`. +* `hideToolbarTop`: Set to `true` to hide the toolbar at the top of the WebView. The default value is `false`. * `toolbarTopBackgroundColor`: Set the custom background color of the toolbar at the top. -* `toolbarTop`: Set to `false` to hide the toolbar at the top of the WebView. The default value is `true`. ##### `InAppBrowser` Android-specific options * `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`. * `hideTitleBar`: Set to `true` if you want the title should be displayed. The default value is `false`. -* `progressBar`: Set to `false` to hide the progress bar at the bottom of the toolbar at the top. The default value is `true`. * `toolbarTopFixedTitle`: Set the action bar's title. ##### `InAppBrowser` iOS-specific options * `closeButtonCaption`: Set the custom text for the close button. * `closeButtonColor`: Set the custom color for the close button. +* `hideToolbarBottom`: Set to `true` to hide the toolbar at the bottom of the WebView. The default value is `false`. * `presentationStyle`: Set the custom modal presentation style when presenting the WebView. The default value is `IOSUIModalPresentationStyle.FULL_SCREEN`. -* `spinner`: Set to `false` to hide the spinner when the WebView is loading a page. The default value is `true`. * `toolbarBottomBackgroundColor`: Set the custom background color of the toolbar at the bottom. +* `toolbarBottomTintColor`: Set the tint color to apply to the bar button items. * `toolbarBottomTranslucent`: Set to `true` to set the toolbar at the bottom translucent. The default value is `true`. -* `toolbarBottom`: Set to `false` to hide the toolbar at the bottom of the WebView. The default value is `true`. +* `toolbarTopTranslucent`: Set to `true` to set the toolbar at the top translucent. The default value is `true`. +* `toolbarTopBarTintColor`: Set the tint color to apply to the navigation bar background. +* `toolbarTopTintColor`: Set the tint color to apply to the navigation items and bar button items. * `transitionStyle`: Set to the custom transition style when presenting the WebView. The default value is `IOSUIModalTransitionStyle.COVER_VERTICAL`. #### `InAppBrowser` Events @@ -1192,8 +1215,6 @@ Specific events of the `InAppBrowser` class are: If you want to use the `ChromeSafariBrowser` class on Android 11+ you need to specify your app querying for `android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml` (you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above). -You can initialize the `ChromeSafariBrowser` instance with an `InAppBrowser` fallback instance. - Create a Class that extends the `ChromeSafariBrowser` Class in order to override the callbacks to manage the browser events. Example: ```dart import 'dart:io'; @@ -1227,8 +1248,6 @@ class MyInAppBrowser extends InAppBrowser { class MyChromeSafariBrowser extends ChromeSafariBrowser { - MyChromeSafariBrowser(browserFallback) : super(bFallback: browserFallback); - @override void onOpened() { print("ChromeSafari browser opened"); @@ -1254,7 +1273,7 @@ Future main() async { } class MyApp extends StatefulWidget { - final ChromeSafariBrowser browser = new MyChromeSafariBrowser(new MyInAppBrowser()); + final ChromeSafariBrowser browser = new MyChromeSafariBrowser(); @override _MyAppState createState() => new _MyAppState(); @@ -1285,10 +1304,10 @@ class _MyAppState extends State { title: const Text('ChromeSafariBrowser Example'), ), body: Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () async { await widget.browser.open( - url: "https://flutter.dev/", + url: Uri.parse("https://flutter.dev/"), options: ChromeSafariBrowserClassOptions( android: AndroidChromeCustomTabsOptions(addDefaultShareMenuItem: false), ios: IOSSafariOptions(barCollapsingEnabled: true))); @@ -1316,7 +1335,7 @@ Screenshots: * `addMenuItems`: Adds a list of `ChromeSafariBrowserMenuItem` to the menu. * `close`: Closes the `ChromeSafariBrowser` instance. * `isOpened`: Returns `true` if the `ChromeSafariBrowser` instance is opened, otherwise `false`. -* `open({required String url, ChromeSafariBrowserClassOptions options, Map headersFallback = const {}, InAppBrowserClassOptions optionsFallback})`: Opens an `url` in a new `ChromeSafariBrowser` instance. +* `open({required Uri url, ChromeSafariBrowserClassOptions? options})`: Opens an `url` in a new `ChromeSafariBrowser` instance. * `static isAvailable`: On Android, returns `true` if Chrome Custom Tabs is available. On iOS, returns `true` if SFSafariViewController is available. Otherwise returns `false`. #### `ChromeSafariBrowser` options @@ -1376,30 +1395,31 @@ Future main() async { title: const Text('InAppWebView Example'), ), body: Container( - child: Column(children: [ - Expanded( - child: Container( - child: InAppWebView( - initialUrl: "http://localhost:8080/assets/index.html", - initialHeaders: {}, - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( + child: Column(children: [ + Expanded( + child: Container( + child: InAppWebView( + initialUrlRequest: URLRequest( + url: Uri.parse("http://localhost:8080/assets/index.html") + ), + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( - ) + ) + ), + onWebViewCreated: (controller) { + + }, + onLoadStart: (controller, url) { + + }, + onLoadStop: (controller, url) { + + }, ), - onWebViewCreated: (controller) { - - }, - onLoadStart: (controller, url) { - - }, - onLoadStop: (controller, url) { - - }, ), - ), - )] - ) + )] + ) ), ), ); @@ -1426,11 +1446,11 @@ On iOS, it is implemented using [WKHTTPCookieStore](https://developer.apple.com/ #### `CookieManager` methods * `instance`: Gets the cookie manager shared instance. -* `setCookie({required String url, required String name, required String value, String? domain, String path = "/", int? expiresDate, int? maxAge, bool? isSecure, bool? isHttpOnly, HTTPCookieSameSitePolicy? sameSite, InAppWebViewController? iosBelow11WebViewController})`: 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. -* `getCookies({required String url, InAppWebViewController? iosBelow11WebViewController})`: Gets all the cookies for the given `url`. -* `getCookie({required String url, required String name, InAppWebViewController? iosBelow11WebViewController})`: Gets a cookie by its `name` for the given `url`. -* `deleteCookie({required String url, required String name, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController})`: Removes a cookie by its `name` for the given `url`, `domain` and `path`. -* `deleteCookies({required String url, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController})`: Removes all cookies for the given `url`, `domain` and `path`. +* `setCookie({required Uri url, required String name, required String value, String? domain, String path = "/", int? expiresDate, int? maxAge, bool? isSecure, bool? isHttpOnly, HTTPCookieSameSitePolicy? sameSite, InAppWebViewController? iosBelow11WebViewController})`: 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. +* `getCookies({required Uri url, InAppWebViewController? iosBelow11WebViewController})`: Gets all the cookies for the given `url`. +* `getCookie({required Uri url, required String name, InAppWebViewController? iosBelow11WebViewController})`: Gets a cookie by its `name` for the given `url`. +* `deleteCookie({required Uri url, required String name, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController})`: Removes a cookie by its `name` for the given `url`, `domain` and `path`. +* `deleteCookies({required Uri url, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController})`: Removes all cookies for the given `url`, `domain` and `path`. * `deleteAllCookies()`: Removes all cookies. #### `CookieManager` iOS-specific methods @@ -1449,10 +1469,10 @@ On Android, this class has a custom implementation using `android.database.sqlit * `instance`: Gets the database shared instance. * `getAllAuthCredentials`: Gets a map list of all HTTP auth credentials saved. -* `getHttpAuthCredentials({required ProtectionSpace protectionSpace})`: Gets all the HTTP auth credentials saved for that `protectionSpace`. -* `setHttpAuthCredential({required ProtectionSpace protectionSpace, required HttpAuthCredential credential})`: Saves an HTTP auth `credential` for that `protectionSpace`. -* `removeHttpAuthCredential({required ProtectionSpace protectionSpace, required HttpAuthCredential credential})`: Removes an HTTP auth `credential` for that `protectionSpace`. -* `removeHttpAuthCredentials({required ProtectionSpace protectionSpace})`: Removes all the HTTP auth credentials saved for that `protectionSpace`. +* `getHttpAuthCredentials({required URLProtectionSpace protectionSpace})`: Gets all the HTTP auth credentials saved for that `protectionSpace`. +* `setHttpAuthCredential({required URLProtectionSpace protectionSpace, required URLCredential credential})`: Saves an HTTP auth `credential` for that `protectionSpace`. +* `removeHttpAuthCredential({required URLProtectionSpace protectionSpace, required URLCredential credential})`: Removes an HTTP auth `credential` for that `protectionSpace`. +* `removeHttpAuthCredentials({required URLProtectionSpace protectionSpace})`: Removes all the HTTP auth credentials saved for that `protectionSpace`. * `clearAllAuthCredentials()`: Removes all the HTTP auth credentials saved in the database. ### `WebStorageManager` class diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 9aaca72f..aa9e7a21 100755 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,9 +2,9 @@ - - - + + + diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeSafariBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeSafariBrowserManager.java deleted file mode 100755 index 3526913c..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeSafariBrowserManager.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ChromeCustomTabsActivity; -import com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.CustomTabActivityHelper; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserOptions; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class ChromeSafariBrowserManager implements MethodChannel.MethodCallHandler { - - public MethodChannel channel; - - protected static final String LOG_TAG = "ChromeBrowserManager"; - - public ChromeSafariBrowserManager(BinaryMessenger messenger) { - channel = new MethodChannel(messenger, "com.pichillilorenzo/flutter_chromesafaribrowser"); - channel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(final MethodCall call, final MethodChannel.Result result) { - final Activity activity = Shared.activity; - final String uuid = (String) call.argument("uuid"); - - switch (call.method) { - case "open": - { - String url = (String) call.argument("url"); - HashMap options = (HashMap) call.argument("options"); - List> menuItemList = (List>) call.argument("menuItemList"); - String uuidFallback = (String) call.argument("uuidFallback"); - Map headersFallback = (Map) call.argument("headersFallback"); - HashMap optionsFallback = (HashMap) call.argument("optionsFallback"); - HashMap contextMenuFallback = (HashMap) call.argument("contextMenuFallback"); - Integer windowIdFallback = (Integer) call.argument("windowIdFallback"); - open(activity, uuid, url, options, menuItemList, uuidFallback, headersFallback, optionsFallback, contextMenuFallback, windowIdFallback, result); - } - break; - case "isAvailable": - result.success(CustomTabActivityHelper.isAvailable(activity)); - break; - default: - result.notImplemented(); - } - } - - public void open(Activity activity, String uuid, String url, HashMap options, List> menuItemList, String uuidFallback, - Map headersFallback, HashMap optionsFallback, HashMap contextMenuFallback, Integer windowIdFallback, - MethodChannel.Result result) { - - Intent intent = null; - Bundle extras = new Bundle(); - extras.putString("fromActivity", activity.getClass().getName()); - extras.putString("url", url); - extras.putBoolean("isData", false); - extras.putString("uuid", uuid); - extras.putSerializable("options", options); - extras.putSerializable("menuItemList", (Serializable) menuItemList); - - extras.putSerializable("headers", (Serializable) headersFallback); - extras.putSerializable("contextMenu", (Serializable) contextMenuFallback); - - extras.putInt("windowId", windowIdFallback != null ? windowIdFallback : -1); - - if (CustomTabActivityHelper.isAvailable(activity)) { - intent = new Intent(activity, ChromeCustomTabsActivity.class); - } - // check for webview fallback - else if (uuidFallback != null) { - Log.d(LOG_TAG, "WebView fallback declared."); - // overwrite with extras fallback parameters - extras.putString("uuid", uuidFallback); - if (optionsFallback != null) - extras.putSerializable("options", optionsFallback); - else - extras.putSerializable("options", (Serializable) (new InAppBrowserOptions()).toMap()); - intent = new Intent(activity, InAppBrowserActivity.class); - } - - if (intent != null) { - intent.putExtras(extras); - activity.startActivity(intent); - result.success(true); - return; - } - - result.error(LOG_TAG, "No WebView fallback declared.", null); - } - - public void dispose() { - channel.setMethodCallHandler(null); - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlocker.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlocker.java deleted file mode 100755 index f9082514..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlocker.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; - -public class ContentBlocker { - public ContentBlockerTrigger trigger; - public ContentBlockerAction action; - - public ContentBlocker (ContentBlockerTrigger trigger, ContentBlockerAction action) { - this.trigger = trigger; - this.action = action; - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerAction.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerAction.java deleted file mode 100755 index b6ff1193..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerAction.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; - -import java.util.Map; - -public class ContentBlockerAction { - ContentBlockerActionType type; - String selector; - - ContentBlockerAction(ContentBlockerActionType type, String selector) { - this.type = type; - if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) { - assert(selector != null); - } - this.selector = selector; - } - - public static ContentBlockerAction fromMap(Map map) { - ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type")); - String selector = (String) map.get("selector"); - return new ContentBlockerAction(type, selector); - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTrigger.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTrigger.java deleted file mode 100755 index 79df6b0c..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTrigger.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -public class ContentBlockerTrigger { - - public String urlFilter; - public Pattern urlFilterPatternCompiled; - public Boolean urlFilterIsCaseSensitive; - public List resourceType = new ArrayList<>(); - public List ifDomain = new ArrayList<>(); - public List unlessDomain = new ArrayList<>(); - public List loadType = new ArrayList<>(); - public List ifTopUrl = new ArrayList<>(); - public List unlessTopUrl = new ArrayList<>(); - - public ContentBlockerTrigger(String urlFilter, Boolean urlFilterIsCaseSensitive, List resourceType, List ifDomain, - List unlessDomain, List loadType, List ifTopUrl, List unlessTopUrl) { - this.urlFilter = urlFilter; - this.urlFilterPatternCompiled = Pattern.compile(this.urlFilter); - - this.resourceType = resourceType != null ? resourceType : this.resourceType; - this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive != null ? urlFilterIsCaseSensitive : false; - this.ifDomain = ifDomain != null ? ifDomain : this.ifDomain; - this.unlessDomain = unlessDomain != null ? unlessDomain : this.unlessDomain; - if ((!(this.ifDomain.isEmpty() || this.unlessDomain.isEmpty()) != false)) - throw new AssertionError(); - this.loadType = loadType != null ? loadType : this.loadType; - if ((this.loadType.size() > 2)) throw new AssertionError(); - this.ifTopUrl = ifTopUrl != null ? ifTopUrl : this.ifTopUrl; - this.unlessTopUrl = unlessTopUrl != null ? unlessTopUrl : this.unlessTopUrl; - if ((!(this.ifTopUrl.isEmpty() || this.unlessTopUrl.isEmpty()) != false)) - throw new AssertionError(); - } - - public static ContentBlockerTrigger fromMap(Map map) { - String urlFilter = (String) map.get("url-filter"); - Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive"); - List resourceTypeStringList = (List) map.get("resource-type"); - List resourceType = new ArrayList<>(); - if (resourceTypeStringList != null) { - for (String type : resourceTypeStringList) { - resourceType.add(ContentBlockerTriggerResourceType.fromValue(type)); - } - } else { - resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values())); - } - List ifDomain = (List) map.get("if-domain"); - List unlessDomain = (List) map.get("unless-domain"); - List loadType = (List) map.get("load-type"); - List ifTopUrl = (List) map.get("if-top-url"); - List unlessTopUrl = (List) map.get("unless-top-url"); - return new ContentBlockerTrigger(urlFilter, urlFilterIsCaseSensitive, resourceType, ifDomain, unlessDomain, loadType, ifTopUrl, unlessTopUrl); - } - -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/Credential.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/Credential.java deleted file mode 100755 index eaed8ade..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/Credential.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import java.util.HashMap; -import java.util.Map; - -public class Credential { - public Long id; - public String username; - public String password; - public Long protectionSpaceId; - - public Credential (Long id, String username, String password, Long protectionSpaceId) { - this.id = id; - this.username = username; - this.password = password; - this.protectionSpaceId = protectionSpaceId; - } - - public Map toMap() { - Map credentialMap = new HashMap<>(); - credentialMap.put("username", username); - credentialMap.put("password", password); - return credentialMap; - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDao.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDao.java deleted file mode 100755 index 6f81b661..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDao.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import android.content.ContentValues; -import android.database.Cursor; - -import java.util.ArrayList; -import java.util.List; - -public class CredentialDao { - - CredentialDatabaseHelper credentialDatabaseHelper; - String[] projection = { - CredentialContract.FeedEntry._ID, - CredentialContract.FeedEntry.COLUMN_NAME_USERNAME, - CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, - CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID - }; - - public CredentialDao(CredentialDatabaseHelper credentialDatabaseHelper) { - this.credentialDatabaseHelper = credentialDatabaseHelper; - } - - public List getAllByProtectionSpaceId(Long protectionSpaceId) { - String selection = CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; - String[] selectionArgs = {protectionSpaceId.toString()}; - - Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( - CredentialContract.FeedEntry.TABLE_NAME, - projection, - selection, - selectionArgs, - null, - null, - null - ); - - List credentials = new ArrayList<>(); - while (cursor.moveToNext()) { - Long id = cursor.getLong(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry._ID)); - String username = cursor.getString(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); - String password = cursor.getString(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); - credentials.add(new Credential(id, username, password, protectionSpaceId)); - } - cursor.close(); - - return credentials; - } - - public Credential find(String username, String password, Long protectionSpaceId) { - String selection = CredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " = ? AND " + - CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " = ? AND " + - CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; - String[] selectionArgs = {username, password, protectionSpaceId.toString()}; - - Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( - CredentialContract.FeedEntry.TABLE_NAME, - projection, - selection, - selectionArgs, - null, - null, - null - ); - - Credential credential = null; - if (cursor.moveToNext()) { - Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry._ID)); - String rowUsername = cursor.getString(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); - String rowPassword = cursor.getString(cursor.getColumnIndexOrThrow(CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); - credential = new Credential(rowId, rowUsername, rowPassword, protectionSpaceId); - } - cursor.close(); - - return credential; - } - - public long insert(Credential credential) { - ContentValues credentialValues = new ContentValues(); - credentialValues.put(CredentialContract.FeedEntry.COLUMN_NAME_USERNAME, credential.username); - credentialValues.put(CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, credential.password); - credentialValues.put(CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID, credential.protectionSpaceId); - - return credentialDatabaseHelper.getWritableDatabase().insert(CredentialContract.FeedEntry.TABLE_NAME, null, credentialValues); - } - - public long update(Credential credential) { - ContentValues credentialValues = new ContentValues(); - credentialValues.put(CredentialContract.FeedEntry.COLUMN_NAME_USERNAME, credential.username); - credentialValues.put(CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, credential.password); - - String whereClause = CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; - String[] whereArgs = {credential.protectionSpaceId.toString()}; - - return credentialDatabaseHelper.getWritableDatabase().update(CredentialContract.FeedEntry.TABLE_NAME, credentialValues, whereClause, whereArgs); - } - - public long delete(Credential credential) { - String whereClause = CredentialContract.FeedEntry._ID + " = ?"; - String[] whereArgs = {credential.id.toString()}; - - return credentialDatabaseHelper.getWritableDatabase().delete(CredentialContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); - } - -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabase.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabase.java deleted file mode 100755 index 9b02fba9..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabase.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import android.content.Context; - -import java.util.ArrayList; -import java.util.List; - -public class CredentialDatabase { - - private static CredentialDatabase instance; - static final String LOG_TAG = "CredentialDatabase"; - - // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 2; - public static final String DATABASE_NAME = "CredentialDatabase.db"; - - public ProtectionSpaceDao protectionSpaceDao; - public CredentialDao credentialDao; - public CredentialDatabaseHelper db; - - private CredentialDatabase() {} - - private CredentialDatabase(CredentialDatabaseHelper db, ProtectionSpaceDao protectionSpaceDao, CredentialDao credentialDao) { - this.db = db; - this.protectionSpaceDao = protectionSpaceDao; - this.credentialDao = credentialDao; - } - - public static CredentialDatabase getInstance(Context context) { - if (instance != null) - return instance; - CredentialDatabaseHelper db = new CredentialDatabaseHelper(context); - instance = new CredentialDatabase(db, new ProtectionSpaceDao(db), new CredentialDao(db)); - return instance; - } - - public List getHttpAuthCredentials(String host, String protocol, String realm, Integer port) { - List credentialList = new ArrayList<>(); - ProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); - if (protectionSpace != null) { - credentialList = credentialDao.getAllByProtectionSpaceId(protectionSpace.id); - } - return credentialList; - } - - public void clearAllAuthCredentials() { - db.clearAllTables(db.getWritableDatabase()); - } - - public void removeHttpAuthCredentials(String host, String protocol, String realm, Integer port) { - ProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); - if (protectionSpace != null) { - protectionSpaceDao.delete(protectionSpace); - } - } - - public void removeHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { - ProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); - if (protectionSpace != null) { - Credential credential = credentialDao.find(username, password, protectionSpace.id); - credentialDao.delete(credential); - } - } - - public void setHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { - ProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); - Long protectionSpaceId; - if (protectionSpace == null) { - protectionSpaceId = protectionSpaceDao.insert(new ProtectionSpace(null, host, protocol, realm, port)); - } else { - protectionSpaceId = protectionSpace.id; - } - - Credential credential = credentialDao.find(username, password, protectionSpaceId); - if (credential != null) { - boolean needUpdate = false; - if (!credential.username.equals(username)) { - credential.username = username; - needUpdate = true; - } - if (!credential.password.equals(password)) { - credential.password = password; - needUpdate = true; - } - if (needUpdate) - credentialDao.update(credential); - } else { - credential = new Credential(null, username, password, protectionSpaceId); - credential.id = credentialDao.insert(credential); - } - } -} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabaseHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabaseHelper.java deleted file mode 100755 index 2cc90777..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialDatabaseHelper.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -public class CredentialDatabaseHelper extends SQLiteOpenHelper { - - private static final String SQL_CREATE_PROTECTION_SPACE_TABLE = - "CREATE TABLE " + ProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + - ProtectionSpaceContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " TEXT NOT NULL," + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " TEXT," + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " TEXT," + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " INTEGER," + - "UNIQUE(" + ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + ", " + ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + ", " + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + ", " + ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + - ")" + - ");"; - - private static final String SQL_CREATE_CREDENTIAL_TABLE = - "CREATE TABLE " + CredentialContract.FeedEntry.TABLE_NAME + " (" + - CredentialContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + - CredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " TEXT NOT NULL," + - CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " TEXT NOT NULL," + - CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " INTEGER NOT NULL," + - "UNIQUE(" + CredentialContract.FeedEntry.COLUMN_NAME_USERNAME + ", " + CredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + ", " + - CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + - ")," + - "FOREIGN KEY (" + CredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + ") REFERENCES " + - ProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + ProtectionSpaceContract.FeedEntry._ID + ") ON DELETE CASCADE" + - ");"; - - private static final String SQL_DELETE_PROTECTION_SPACE_TABLE = - "DROP TABLE IF EXISTS " + ProtectionSpaceContract.FeedEntry.TABLE_NAME; - - private static final String SQL_DELETE_CREDENTIAL_TABLE = - "DROP TABLE IF EXISTS " + CredentialContract.FeedEntry.TABLE_NAME; - - public CredentialDatabaseHelper(Context context) { - super(context, CredentialDatabase.DATABASE_NAME, null, CredentialDatabase.DATABASE_VERSION); - } - - public void onCreate(SQLiteDatabase db) { - db.execSQL(SQL_CREATE_PROTECTION_SPACE_TABLE); - db.execSQL(SQL_CREATE_CREDENTIAL_TABLE); - } - - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // This database is only a cache for online data, so its upgrade policy is - // to simply to discard the data and start over - db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); - db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); - onCreate(db); - } - - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onUpgrade(db, oldVersion, newVersion); - } - - public void clearAllTables(SQLiteDatabase db) { - db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); - db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); - onCreate(db); - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpace.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpace.java deleted file mode 100755 index 1e460fe1..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpace.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import java.util.HashMap; -import java.util.Map; - -public class ProtectionSpace { - public Long id; - public String host; - public String procotol; - public String realm; - public Integer port; - - public ProtectionSpace (Long id, String host, String protocol, String realm, Integer port) { - this.id = id; - this.host = host; - this.procotol = protocol; - this.realm = realm; - this.port = port; - } - - public Map toMap() { - Map protectionSpaceMap = new HashMap<>(); - protectionSpaceMap.put("host", host); - protectionSpaceMap.put("protocol", procotol); - protectionSpaceMap.put("realm", realm); - protectionSpaceMap.put("port", port); - return protectionSpaceMap; - } -} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceDao.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceDao.java deleted file mode 100755 index 81d1cdfc..00000000 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceDao.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -import java.util.ArrayList; -import java.util.List; - -public class ProtectionSpaceDao { - CredentialDatabaseHelper credentialDatabaseHelper; - String[] projection = { - ProtectionSpaceContract.FeedEntry._ID, - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT - }; - - public ProtectionSpaceDao(CredentialDatabaseHelper credentialDatabaseHelper) { - this.credentialDatabaseHelper = credentialDatabaseHelper; - } - - public List getAll() { - SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); - - Cursor cursor = readableDatabase.query( - ProtectionSpaceContract.FeedEntry.TABLE_NAME, - projection, - null, - null, - null, - null, - null - ); - - List protectionSpaces = new ArrayList<>(); - while (cursor.moveToNext()) { - Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry._ID)); - String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); - String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); - String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); - Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); - protectionSpaces.add(new ProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort)); - } - cursor.close(); - - return protectionSpaces; - } - - public ProtectionSpace find(String host, String protocol, String realm, Integer port) { - SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); - - String selection = ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " = ? AND " + ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " = ? AND " + - ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " = ? AND " + ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " = ?"; - String[] selectionArgs = {host, protocol, realm, port.toString()}; - - Cursor cursor = readableDatabase.query( - ProtectionSpaceContract.FeedEntry.TABLE_NAME, - projection, - selection, - selectionArgs, - null, - null, - null - ); - - ProtectionSpace protectionSpace = null; - if (cursor.moveToNext()) { - Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry._ID)); - String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); - String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); - String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); - Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); - protectionSpace = new ProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort); - } - cursor.close(); - - return protectionSpace; - } - - public long insert(ProtectionSpace protectionSpace) { - ContentValues protectionSpaceValues = new ContentValues(); - protectionSpaceValues.put(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, protectionSpace.host); - protectionSpaceValues.put(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, protectionSpace.procotol); - protectionSpaceValues.put(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, protectionSpace.realm); - protectionSpaceValues.put(ProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT, protectionSpace.port); - - return credentialDatabaseHelper.getWritableDatabase().insert(ProtectionSpaceContract.FeedEntry.TABLE_NAME, null, protectionSpaceValues); - }; - - public long delete(ProtectionSpace protectionSpace) { - String whereClause = ProtectionSpaceContract.FeedEntry._ID + " = ?"; - String[] whereArgs = {protectionSpace.id.toString()}; - - return credentialDatabaseHelper.getWritableDatabase().delete(ProtectionSpaceContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); - } -} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java index 4af050b7..645d5f19 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java @@ -4,10 +4,13 @@ import android.app.Activity; import android.content.Context; import android.net.Uri; import android.os.Build; -import android.util.Log; import android.webkit.ValueCallback; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.FlutterWebViewFactory; +import com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs.ChromeSafariBrowserManager; +import com.pichillilorenzo.flutter_inappwebview.credential_database.CredentialDatabaseHandler; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserManager; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.FlutterWebViewFactory; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.HeadlessInAppWebViewManager; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -15,7 +18,6 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.platform.PlatformViewRegistry; -import io.flutter.view.FlutterMain; import io.flutter.view.FlutterView; public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java index a7e40cfd..17a5aa0a 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java @@ -8,10 +8,16 @@ import androidx.annotation.NonNull; import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserOptions; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebView; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebViewOptions; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserOptions; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebViewOptions; +import com.pichillilorenzo.flutter_inappwebview.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview.types.SslCertificateExt; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview.types.UserScript; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -40,42 +46,55 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle result.success((webView != null) ? webView.getProgress() : null); break; case "loadUrl": - if (webView != null) - webView.loadUrl((String) call.argument("url"), (Map) call.argument("headers"), result); - else - result.success(false); + if (webView != null) { + Map urlRequest = (Map) call.argument("urlRequest"); + webView.loadUrl(URLRequest.fromMap(urlRequest)); + } + result.success(true); break; case "postUrl": - if (webView != null) - webView.postUrl((String) call.argument("url"), (byte[]) call.argument("postData"), result); - else - result.success(false); + if (webView != null) { + String url = (String) call.argument("url"); + byte[] postData = (byte[]) call.argument("postData"); + webView.postUrl(url, postData); + } + result.success(true); break; case "loadData": - { - String data = (String) call.argument("data"); - String mimeType = (String) call.argument("mimeType"); - String encoding = (String) call.argument("encoding"); - String baseUrl = (String) call.argument("baseUrl"); - String historyUrl = (String) call.argument("historyUrl"); - - if (webView != null) - webView.loadData(data, mimeType, encoding, baseUrl, historyUrl, result); - else - result.success(false); - } + if (webView != null) { + String data = (String) call.argument("data"); + String mimeType = (String) call.argument("mimeType"); + String encoding = (String) call.argument("encoding"); + String baseUrl = (String) call.argument("baseUrl"); + String historyUrl = (String) call.argument("historyUrl"); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + result.success(true); break; case "loadFile": - if (webView != null) - webView.loadFile((String) call.argument("url"), (Map) call.argument("headers"), result); - else - result.success(false); + if (webView != null) { + String assetFilePath = (String) call.argument("url"); + try { + webView.loadFile(assetFilePath); + } catch (IOException e) { + e.printStackTrace(); + result.error(LOG_TAG, e.getMessage(), null); + return; + } + } + result.success(true); break; case "evaluateJavascript": if (webView != null) { String source = (String) call.argument("source"); - String contentWorldName = (String) call.argument("contentWorld"); - webView.evaluateJavascript(source, contentWorldName, result); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.evaluateJavascript(source, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); } else { result.success(null); @@ -150,11 +169,12 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle result.success(null); break; case "setOptions": - if (webView != null && webView.inAppBrowserActivity != null) { + if (webView != null && webView.inAppBrowserDelegate != null && webView.inAppBrowserDelegate instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.inAppBrowserDelegate; InAppBrowserOptions inAppBrowserOptions = new InAppBrowserOptions(); HashMap inAppBrowserOptionsMap = (HashMap) call.argument("options"); inAppBrowserOptions.parse(inAppBrowserOptionsMap); - webView.inAppBrowserActivity.setOptions(inAppBrowserOptions, inAppBrowserOptionsMap); + inAppBrowserActivity.setOptions(inAppBrowserOptions, inAppBrowserOptionsMap); } else if (webView != null) { InAppWebViewOptions inAppWebViewOptions = new InAppWebViewOptions(); HashMap inAppWebViewOptionsMap = (HashMap) call.argument("options"); @@ -164,30 +184,34 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle result.success(true); break; case "getOptions": - if (webView != null && webView.inAppBrowserActivity != null) { - result.success(webView.inAppBrowserActivity.getOptions()); + if (webView != null && webView.inAppBrowserDelegate != null && webView.inAppBrowserDelegate instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.inAppBrowserDelegate; + result.success(inAppBrowserActivity.getOptions()); } else { result.success((webView != null) ? webView.getOptions() : null); } break; case "close": - if (webView != null && webView.inAppBrowserActivity != null) { - webView.inAppBrowserActivity.close(result); + if (webView != null && webView.inAppBrowserDelegate != null && webView.inAppBrowserDelegate instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.inAppBrowserDelegate; + inAppBrowserActivity.close(result); } else { result.notImplemented(); } break; case "show": - if (webView != null && webView.inAppBrowserActivity != null) { - webView.inAppBrowserActivity.show(); + if (webView != null && webView.inAppBrowserDelegate != null && webView.inAppBrowserDelegate instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.inAppBrowserDelegate; + inAppBrowserActivity.show(); result.success(true); } else { result.notImplemented(); } break; case "hide": - if (webView != null && webView.inAppBrowserActivity != null) { - webView.inAppBrowserActivity.hide(); + if (webView != null && webView.inAppBrowserDelegate != null && webView.inAppBrowserDelegate instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.inAppBrowserDelegate; + inAppBrowserActivity.hide(); result.success(true); } else { result.notImplemented(); @@ -409,7 +433,7 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle break; case "getCertificate": if (webView != null) { - result.success(webView.getCertificateMap()); + result.success(SslCertificateExt.toMap(webView.getCertificate())); } else { result.success(null); } @@ -421,24 +445,34 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle result.success(true); break; case "addUserScript": - if (webView != null) { - Map userScript = (Map) call.argument("userScript"); - result.success(webView.addUserScript(userScript)); + if (webView != null && webView.userContentController != null) { + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.userContentController.addUserOnlyScript(userScript)); } else { result.success(false); } break; case "removeUserScript": - if (webView != null) { + if (webView != null && webView.userContentController != null) { Integer index = (Integer) call.argument("index"); - result.success(webView.removeUserScript(index)); + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.userContentController.removePluginScriptAt(index, userScript.getInjectionTime())); } else { result.success(false); } break; + case "removeUserScriptsByGroupName": + if (webView != null && webView.userContentController != null) { + String groupName = (String) call.argument("groupName"); + webView.userContentController.removeUserOnlyScriptsByGroupName(groupName); + } + result.success(true); + break; case "removeAllUserScripts": - if (webView != null) { - webView.removeAllUserScripts(); + if (webView != null && webView.userContentController != null) { + webView.userContentController.removeAllUserOnlyScripts(); } result.success(true); break; @@ -446,13 +480,31 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { String functionBody = (String) call.argument("functionBody"); Map functionArguments = (Map) call.argument("arguments"); - String contentWorldName = (String) call.argument("contentWorld"); - webView.callAsyncJavaScript(functionBody, functionArguments, contentWorldName, result); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.callAsyncJavaScript(functionBody, functionArguments, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); } else { result.success(null); } break; + case "isSecureContext": + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.isSecureContext(new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } else { + result.success(false); + } + break; default: result.notImplemented(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java index 56d1b481..45e3a233 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java @@ -7,9 +7,8 @@ import android.util.Log; import android.webkit.JavascriptInterface; import android.webkit.ValueCallback; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.FlutterWebView; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import org.json.JSONArray; import org.json.JSONException; @@ -22,58 +21,27 @@ import io.flutter.plugin.common.MethodChannel; public class JavaScriptBridgeInterface { private static final String LOG_TAG = "JSBridgeInterface"; - public static final String name = "flutter_inappwebview"; - private FlutterWebView flutterWebView; - private InAppBrowserActivity inAppBrowserActivity; - public MethodChannel channel; + private InAppWebView inAppWebView; + private final MethodChannel channel; - // https://github.com/tildeio/rsvp.js - public static final String promisePolyfillJS = "if (window.Promise == null) {" + - " !function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.RSVP={})}(this,function(t){\"use strict\";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if(\"function\"!=typeof r)throw new TypeError(\"Callback must be a function\");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return!e||\"object\"!=typeof e&&\"function\"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if(\"function\"!=typeof e&&null!==e)throw new TypeError(\"Super expression must either be null or a function, not \"+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s obj = new HashMap<>(); obj.put("handlerName", handlerName); obj.put("args", args); @@ -97,19 +63,36 @@ public class JavaScriptBridgeInterface { handler.post(new Runnable() { @Override public void run() { + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } if (handlerName.equals("onPrint") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.printCurrentPage(); + inAppWebView.printCurrentPage(); } else if (handlerName.equals("callAsyncJavaScript")) { try { JSONArray arguments = new JSONArray(args); JSONObject jsonObject = arguments.getJSONObject(0); String resultUuid = jsonObject.getString("resultUuid"); - if (webView.callAsyncJavaScriptResults.containsKey(resultUuid)) { - MethodChannel.Result callAsyncJavaScriptResult = webView.callAsyncJavaScriptResults.get(resultUuid); - callAsyncJavaScriptResult.success(jsonObject.toString()); - - webView.callAsyncJavaScriptResults.remove(resultUuid); + ValueCallback callAsyncJavaScriptCallback = inAppWebView.callAsyncJavaScriptCallbacks.get(resultUuid); + if (callAsyncJavaScriptCallback != null) { + callAsyncJavaScriptCallback.onReceiveValue(jsonObject.toString()); + inAppWebView.callAsyncJavaScriptCallbacks.remove(resultUuid); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return; + } else if (handlerName.equals("evaluateJavaScriptWithContentWorld")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + ValueCallback evaluateJavaScriptCallback = inAppWebView.evaluateJavaScriptContentWorldCallbacks.get(resultUuid); + if (evaluateJavaScriptCallback != null) { + evaluateJavaScriptCallback.onReceiveValue(jsonObject.has("value") ? jsonObject.get("value").toString() : "null"); + inAppWebView.evaluateJavaScriptContentWorldCallbacks.remove(resultUuid); } } catch (JSONException e) { e.printStackTrace(); @@ -117,18 +100,19 @@ public class JavaScriptBridgeInterface { return; } + // invoke flutter javascript handler and send back flutter data as a JSON Object to javascript channel.invokeMethod("onCallJsHandler", obj, new MethodChannel.Result() { @Override public void success(Object json) { - if (webView == null) { + if (inAppWebView == null) { // The webview has already been disposed, ignore. return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.evaluateJavascript("if(window." + name + "[" + _callHandlerID + "] != null) {window." + name + "[" + _callHandlerID + "](" + json + "); delete window." + name + "[" + _callHandlerID + "];}", (ValueCallback) null); + inAppWebView.evaluateJavascript("if(window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) {window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "](" + json + "); delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "];}", (ValueCallback) null); } else { - webView.loadUrl("javascript:if(window." + name + "[" + _callHandlerID + "] != null) {window." + name + "[" + _callHandlerID + "](" + json + "); delete window." + name + "[" + _callHandlerID + "];}"); + inAppWebView.loadUrl("javascript:if(window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) {window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "](" + json + "); delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "];}"); } } @@ -147,12 +131,6 @@ public class JavaScriptBridgeInterface { } public void dispose() { - channel.setMethodCallHandler(null); - if (inAppBrowserActivity != null) { - inAppBrowserActivity = null; - } - if (flutterWebView != null) { - flutterWebView = null; - } + inAppWebView = null; } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java index 7a3a88b1..4a0b45b1 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java @@ -8,8 +8,10 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import org.json.JSONArray; @@ -27,12 +29,15 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -244,7 +249,7 @@ public class Util { } @RequiresApi(api = Build.VERSION_CODES.KITKAT) - public static String JSONStringify(Object value) { + public static String JSONStringify(@Nullable Object value) { if (value == null) { return "null"; } @@ -258,4 +263,15 @@ public class Util { return JSONObject.wrap(value).toString(); } } + + public static boolean objEquals(@Nullable Object a, @Nullable Object b) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return Objects.equals(a, b); + } + return (a == b) || (a != null && a.equals(b)); + } + + public static String replaceAll(String s, String oldString, String newString) { + return TextUtils.join(newString, s.split(Pattern.quote(oldString))); + } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ActionBroadcastReceiver.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java similarity index 93% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ActionBroadcastReceiver.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java index f2a439f6..04557756 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ActionBroadcastReceiver.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java @@ -1,10 +1,9 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import com.pichillilorenzo.flutter_inappwebview.Shared; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java similarity index 97% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsActivity.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java index 85d9fdae..0de7ce9b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.app.Activity; import android.app.PendingIntent; @@ -6,7 +6,6 @@ import android.content.Intent; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; -import android.util.Log; import androidx.browser.customtabs.CustomTabsCallback; import androidx.browser.customtabs.CustomTabsIntent; @@ -16,7 +15,6 @@ import androidx.browser.customtabs.CustomTabsSession; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; -import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -145,7 +143,7 @@ public class ChromeCustomTabsActivity extends Activity implements MethodChannel. if (options.addDefaultShareMenuItem) builder.addDefaultShareMenuItem(); - if (!options.toolbarBackgroundColor.isEmpty()) + if (options.toolbarBackgroundColor != null && !options.toolbarBackgroundColor.isEmpty()) builder.setToolbarColor(Color.parseColor(options.toolbarBackgroundColor)); builder.setShowTitle(options.showTitle); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java similarity index 94% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsOptions.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java index 005e9fa7..7e04628b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ChromeCustomTabsOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java @@ -1,7 +1,9 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.content.Intent; +import androidx.annotation.Nullable; + import com.pichillilorenzo.flutter_inappwebview.Options; import java.util.HashMap; @@ -13,7 +15,8 @@ public class ChromeCustomTabsOptions implements Options options = (HashMap) call.argument("options"); + List> menuItemList = (List>) call.argument("menuItemList"); + open(activity, uuid, url, options, menuItemList, result); + } + break; + case "isAvailable": + result.success(CustomTabActivityHelper.isAvailable(activity)); + break; + default: + result.notImplemented(); + } + } + + public void open(Activity activity, String uuid, String url, HashMap options, + List> menuItemList, MethodChannel.Result result) { + + Intent intent = null; + Bundle extras = new Bundle(); + extras.putString("fromActivity", activity.getClass().getName()); + extras.putString("url", url); + extras.putBoolean("isData", false); + extras.putString("uuid", uuid); + extras.putSerializable("options", options); + extras.putSerializable("menuItemList", (Serializable) menuItemList); + + if (CustomTabActivityHelper.isAvailable(activity)) { + intent = new Intent(activity, ChromeCustomTabsActivity.class); + intent.putExtras(extras); + activity.startActivity(intent); + result.success(true); + return; + } + + result.error(LOG_TAG, "ChromeCustomTabs is not available!", null); + } + + public void dispose() { + channel.setMethodCallHandler(null); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabActivityHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java similarity index 98% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabActivityHelper.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java index 95bc7fea..31c348c3 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabActivityHelper.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.app.Activity; import android.net.Uri; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabsHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabsHelper.java similarity index 98% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabsHelper.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabsHelper.java index a807e5f2..c3f0198d 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/CustomTabsHelper.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabsHelper.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.content.Context; import android.content.Intent; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/KeepAliveService.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/KeepAliveService.java similarity index 85% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/KeepAliveService.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/KeepAliveService.java index 5b936b1c..b1302f99 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/KeepAliveService.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/KeepAliveService.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.app.Service; import android.content.Intent; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnection.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnection.java similarity index 94% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnection.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnection.java index 0cbea7cf..33c3058f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnection.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnection.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.content.ComponentName; import androidx.browser.customtabs.CustomTabsClient; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnectionCallback.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnectionCallback.java similarity index 86% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnectionCallback.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnectionCallback.java index fdf94209..6f792300 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ChromeCustomTabs/ServiceConnectionCallback.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ServiceConnectionCallback.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs; +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import androidx.browser.customtabs.CustomTabsClient; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlocker.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlocker.java new file mode 100755 index 00000000..21c4af73 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlocker.java @@ -0,0 +1,59 @@ +package com.pichillilorenzo.flutter_inappwebview.content_blocker; + +import androidx.annotation.NonNull; + +public class ContentBlocker { + @NonNull + private ContentBlockerTrigger trigger; + @NonNull + private ContentBlockerAction action; + + public ContentBlocker (@NonNull ContentBlockerTrigger trigger, @NonNull ContentBlockerAction action) { + this.trigger = trigger; + this.action = action; + } + + @NonNull + public ContentBlockerTrigger getTrigger() { + return trigger; + } + + public void setTrigger(@NonNull ContentBlockerTrigger trigger) { + this.trigger = trigger; + } + + @NonNull + public ContentBlockerAction getAction() { + return action; + } + + public void setAction(@NonNull ContentBlockerAction action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlocker that = (ContentBlocker) o; + + if (!trigger.equals(that.trigger)) return false; + return action.equals(that.action); + } + + @Override + public int hashCode() { + int result = trigger.hashCode(); + result = 31 * result + action.hashCode(); + return result; + } + + @Override + public String toString() { + return "ContentBlocker{" + + "trigger=" + trigger + + ", action=" + action + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerAction.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerAction.java new file mode 100755 index 00000000..072413c0 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerAction.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview.content_blocker; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ContentBlockerAction { + @NonNull + private ContentBlockerActionType type; + + @Nullable + private String selector; + + ContentBlockerAction(@NonNull ContentBlockerActionType type, @Nullable String selector) { + this.type = type; + if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) { + assert(selector != null); + } + this.selector = selector; + } + + public static ContentBlockerAction fromMap(Map map) { + ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type")); + String selector = (String) map.get("selector"); + return new ContentBlockerAction(type, selector); + } + + @NonNull + public ContentBlockerActionType getType() { + return type; + } + + public void setType(@NonNull ContentBlockerActionType type) { + this.type = type; + } + + public String getSelector() { + return selector; + } + + public void setSelector(String selector) { + this.selector = selector; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlockerAction that = (ContentBlockerAction) o; + + if (type != that.type) return false; + return selector != null ? selector.equals(that.selector) : that.selector == null; + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + (selector != null ? selector.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ContentBlockerAction{" + + "type=" + type + + ", selector='" + selector + '\'' + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerActionType.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerActionType.java similarity index 90% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerActionType.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerActionType.java index 8a8601ad..8ed16c8f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerActionType.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerActionType.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; +package com.pichillilorenzo.flutter_inappwebview.content_blocker; public enum ContentBlockerActionType { BLOCK ("block"), @@ -23,6 +23,7 @@ public enum ContentBlockerActionType { throw new IllegalArgumentException("No enum constant: " + value); } + @Override public String toString() { return this.value; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerHandler.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerHandler.java similarity index 88% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerHandler.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerHandler.java index 546dcd45..aac86a7e 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerHandler.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerHandler.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; +package com.pichillilorenzo.flutter_inappwebview.content_blocker; import android.os.Build; import android.os.Handler; @@ -6,7 +6,7 @@ import android.os.Looper; import android.util.Log; import android.webkit.WebResourceResponse; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView; import com.pichillilorenzo.flutter_inappwebview.Util; import java.io.ByteArrayInputStream; @@ -21,7 +21,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.regex.Matcher; -import io.flutter.plugin.common.MethodChannel; import okhttp3.Request; import okhttp3.Response; @@ -64,23 +63,23 @@ public class ContentBlockerHandler { List ruleListCopy = new CopyOnWriteArrayList(ruleList); for (ContentBlocker contentBlocker : ruleListCopy) { - ContentBlockerTrigger trigger = contentBlocker.trigger; - List resourceTypes = trigger.resourceType; + ContentBlockerTrigger trigger = contentBlocker.getTrigger(); + List resourceTypes = trigger.getResourceType(); if (resourceTypes.contains(ContentBlockerTriggerResourceType.IMAGE) && !resourceTypes.contains(ContentBlockerTriggerResourceType.SVG_DOCUMENT)) { resourceTypes.add(ContentBlockerTriggerResourceType.SVG_DOCUMENT); } - ContentBlockerAction action = contentBlocker.action; + ContentBlockerAction action = contentBlocker.getAction(); - Matcher m = trigger.urlFilterPatternCompiled.matcher(url); + Matcher m = trigger.getUrlFilterPatternCompiled().matcher(url); if (m.matches()) { if (!resourceTypes.isEmpty() && !resourceTypes.contains(responseResourceType)) { return null; } - if (!trigger.ifDomain.isEmpty()) { + if (!trigger.getIfDomain().isEmpty()) { boolean matchFound = false; - for (String domain : trigger.ifDomain) { + for (String domain : trigger.getIfDomain()) { if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) { matchFound = true; break; @@ -89,14 +88,14 @@ public class ContentBlockerHandler { if (!matchFound) return null; } - if (!trigger.unlessDomain.isEmpty()) { - for (String domain : trigger.unlessDomain) + if (!trigger.getUnlessDomain().isEmpty()) { + for (String domain : trigger.getUnlessDomain()) if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) return null; } final String[] webViewUrl = new String[1]; - if (!trigger.loadType.isEmpty() || !trigger.ifTopUrl.isEmpty() || !trigger.unlessTopUrl.isEmpty()) { + if (!trigger.getLoadType().isEmpty() || !trigger.getIfTopUrl().isEmpty() || !trigger.getUnlessTopUrl().isEmpty()) { final CountDownLatch latch = new CountDownLatch(1); Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @@ -110,19 +109,19 @@ public class ContentBlockerHandler { } if (webViewUrl[0] != null) { - if (!trigger.loadType.isEmpty()) { + if (!trigger.getLoadType().isEmpty()) { URI cUrl = new URI(webViewUrl[0]); String cHost = cUrl.getHost(); int cPort = cUrl.getPort(); String cScheme = cUrl.getScheme(); - if ( (trigger.loadType.contains("first-party") && cHost != null && !(cScheme.equals(scheme) && cHost.equals(host) && cPort == port)) || - (trigger.loadType.contains("third-party") && cHost != null && cHost.equals(host)) ) + if ( (trigger.getLoadType().contains("first-party") && cHost != null && !(cScheme.equals(scheme) && cHost.equals(host) && cPort == port)) || + (trigger.getLoadType().contains("third-party") && cHost != null && cHost.equals(host)) ) return null; } - if (!trigger.ifTopUrl.isEmpty()) { + if (!trigger.getIfTopUrl().isEmpty()) { boolean matchFound = false; - for (String topUrl : trigger.ifTopUrl) { + for (String topUrl : trigger.getIfTopUrl()) { if (webViewUrl[0].startsWith(topUrl)) { matchFound = true; break; @@ -131,20 +130,20 @@ public class ContentBlockerHandler { if (!matchFound) return null; } - if (!trigger.unlessTopUrl.isEmpty()) { - for (String topUrl : trigger.unlessTopUrl) + if (!trigger.getUnlessTopUrl().isEmpty()) { + for (String topUrl : trigger.getUnlessTopUrl()) if (webViewUrl[0].startsWith(topUrl)) return null; } } - switch (action.type) { + switch (action.getType()) { case BLOCK: return new WebResourceResponse("", "", null); case CSS_DISPLAY_NONE: - final String cssSelector = action.selector; + final String cssSelector = action.getSelector(); final String jsScript = "(function(d) { " + " function hide () { " + " if (!d.getElementById('css-display-none-style')) { " + diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTrigger.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTrigger.java new file mode 100755 index 00000000..8310ede1 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTrigger.java @@ -0,0 +1,184 @@ +package com.pichillilorenzo.flutter_inappwebview.content_blocker; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class ContentBlockerTrigger { + + @NonNull + private String urlFilter; + private Pattern urlFilterPatternCompiled; + private Boolean urlFilterIsCaseSensitive; + private List resourceType = new ArrayList<>(); + private List ifDomain = new ArrayList<>(); + private List unlessDomain = new ArrayList<>(); + private List loadType = new ArrayList<>(); + private List ifTopUrl = new ArrayList<>(); + private List unlessTopUrl = new ArrayList<>(); + + public ContentBlockerTrigger(@NonNull String urlFilter, @Nullable Boolean urlFilterIsCaseSensitive, @Nullable List resourceType, + @Nullable List ifDomain, @Nullable List unlessDomain, @Nullable List loadType, + @Nullable List ifTopUrl, @Nullable List unlessTopUrl) { + this.urlFilter = urlFilter; + this.urlFilterPatternCompiled = Pattern.compile(this.urlFilter); + + this.resourceType = resourceType != null ? resourceType : this.resourceType; + this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive != null ? urlFilterIsCaseSensitive : false; + this.ifDomain = ifDomain != null ? ifDomain : this.ifDomain; + this.unlessDomain = unlessDomain != null ? unlessDomain : this.unlessDomain; + if ((!(this.ifDomain.isEmpty() || this.unlessDomain.isEmpty()) != false)) + throw new AssertionError(); + this.loadType = loadType != null ? loadType : this.loadType; + if ((this.loadType.size() > 2)) throw new AssertionError(); + this.ifTopUrl = ifTopUrl != null ? ifTopUrl : this.ifTopUrl; + this.unlessTopUrl = unlessTopUrl != null ? unlessTopUrl : this.unlessTopUrl; + if ((!(this.ifTopUrl.isEmpty() || this.unlessTopUrl.isEmpty()) != false)) + throw new AssertionError(); + } + + public static ContentBlockerTrigger fromMap(Map map) { + String urlFilter = (String) map.get("url-filter"); + Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive"); + List resourceTypeStringList = (List) map.get("resource-type"); + List resourceType = new ArrayList<>(); + if (resourceTypeStringList != null) { + for (String type : resourceTypeStringList) { + resourceType.add(ContentBlockerTriggerResourceType.fromValue(type)); + } + } else { + resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values())); + } + List ifDomain = (List) map.get("if-domain"); + List unlessDomain = (List) map.get("unless-domain"); + List loadType = (List) map.get("load-type"); + List ifTopUrl = (List) map.get("if-top-url"); + List unlessTopUrl = (List) map.get("unless-top-url"); + return new ContentBlockerTrigger(urlFilter, urlFilterIsCaseSensitive, resourceType, ifDomain, unlessDomain, loadType, ifTopUrl, unlessTopUrl); + } + + @NonNull + public String getUrlFilter() { + return urlFilter; + } + + public void setUrlFilter(@NonNull String urlFilter) { + this.urlFilter = urlFilter; + } + + public Pattern getUrlFilterPatternCompiled() { + return urlFilterPatternCompiled; + } + + public void setUrlFilterPatternCompiled(Pattern urlFilterPatternCompiled) { + this.urlFilterPatternCompiled = urlFilterPatternCompiled; + } + + public Boolean getUrlFilterIsCaseSensitive() { + return urlFilterIsCaseSensitive; + } + + public void setUrlFilterIsCaseSensitive(Boolean urlFilterIsCaseSensitive) { + this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive; + } + + public List getResourceType() { + return resourceType; + } + + public void setResourceType(List resourceType) { + this.resourceType = resourceType; + } + + public List getIfDomain() { + return ifDomain; + } + + public void setIfDomain(List ifDomain) { + this.ifDomain = ifDomain; + } + + public List getUnlessDomain() { + return unlessDomain; + } + + public void setUnlessDomain(List unlessDomain) { + this.unlessDomain = unlessDomain; + } + + public List getLoadType() { + return loadType; + } + + public void setLoadType(List loadType) { + this.loadType = loadType; + } + + public List getIfTopUrl() { + return ifTopUrl; + } + + public void setIfTopUrl(List ifTopUrl) { + this.ifTopUrl = ifTopUrl; + } + + public List getUnlessTopUrl() { + return unlessTopUrl; + } + + public void setUnlessTopUrl(List unlessTopUrl) { + this.unlessTopUrl = unlessTopUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentBlockerTrigger that = (ContentBlockerTrigger) o; + + if (!urlFilter.equals(that.urlFilter)) return false; + if (!urlFilterPatternCompiled.equals(that.urlFilterPatternCompiled)) return false; + if (!urlFilterIsCaseSensitive.equals(that.urlFilterIsCaseSensitive)) return false; + if (!resourceType.equals(that.resourceType)) return false; + if (!ifDomain.equals(that.ifDomain)) return false; + if (!unlessDomain.equals(that.unlessDomain)) return false; + if (!loadType.equals(that.loadType)) return false; + if (!ifTopUrl.equals(that.ifTopUrl)) return false; + return unlessTopUrl.equals(that.unlessTopUrl); + } + + @Override + public int hashCode() { + int result = urlFilter.hashCode(); + result = 31 * result + urlFilterPatternCompiled.hashCode(); + result = 31 * result + urlFilterIsCaseSensitive.hashCode(); + result = 31 * result + resourceType.hashCode(); + result = 31 * result + ifDomain.hashCode(); + result = 31 * result + unlessDomain.hashCode(); + result = 31 * result + loadType.hashCode(); + result = 31 * result + ifTopUrl.hashCode(); + result = 31 * result + unlessTopUrl.hashCode(); + return result; + } + + @Override + public String toString() { + return "ContentBlockerTrigger{" + + "urlFilter='" + urlFilter + '\'' + + ", urlFilterPatternCompiled=" + urlFilterPatternCompiled + + ", urlFilterIsCaseSensitive=" + urlFilterIsCaseSensitive + + ", resourceType=" + resourceType + + ", ifDomain=" + ifDomain + + ", unlessDomain=" + unlessDomain + + ", loadType=" + loadType + + ", ifTopUrl=" + ifTopUrl + + ", unlessTopUrl=" + unlessTopUrl + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTriggerResourceType.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTriggerResourceType.java similarity index 91% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTriggerResourceType.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTriggerResourceType.java index fc78ead5..0f63196d 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/ContentBlocker/ContentBlockerTriggerResourceType.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/content_blocker/ContentBlockerTriggerResourceType.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.ContentBlocker; +package com.pichillilorenzo.flutter_inappwebview.content_blocker; public enum ContentBlockerTriggerResourceType { DOCUMENT ("document"), @@ -29,6 +29,7 @@ public enum ContentBlockerTriggerResourceType { throw new IllegalArgumentException("No enum constant: " + value); } + @Override public String toString() { return this.value; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabase.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabase.java new file mode 100755 index 00000000..c85a4d60 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabase.java @@ -0,0 +1,95 @@ +package com.pichillilorenzo.flutter_inappwebview.credential_database; + +import android.content.Context; + +import com.pichillilorenzo.flutter_inappwebview.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview.types.URLProtectionSpace; + +import java.util.ArrayList; +import java.util.List; + +public class CredentialDatabase { + + private static CredentialDatabase instance; + static final String LOG_TAG = "CredentialDatabase"; + + // If you change the database schema, you must increment the database version. + public static final int DATABASE_VERSION = 2; + public static final String DATABASE_NAME = "CredentialDatabase.db"; + + public URLProtectionSpaceDao protectionSpaceDao; + public URLCredentialDao credentialDao; + public CredentialDatabaseHelper db; + + private CredentialDatabase() {} + + private CredentialDatabase(CredentialDatabaseHelper db, URLProtectionSpaceDao protectionSpaceDao, URLCredentialDao credentialDao) { + this.db = db; + this.protectionSpaceDao = protectionSpaceDao; + this.credentialDao = credentialDao; + } + + public static CredentialDatabase getInstance(Context context) { + if (instance != null) + return instance; + CredentialDatabaseHelper db = new CredentialDatabaseHelper(context); + instance = new CredentialDatabase(db, new URLProtectionSpaceDao(db), new URLCredentialDao(db)); + return instance; + } + + public List getHttpAuthCredentials(String host, String protocol, String realm, Integer port) { + List credentials = new ArrayList<>(); + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (protectionSpace != null) { + credentials = credentialDao.getAllByProtectionSpaceId(protectionSpace.getId()); + } + return credentials; + } + + public void clearAllAuthCredentials() { + db.clearAllTables(db.getWritableDatabase()); + } + + public void removeHttpAuthCredentials(String host, String protocol, String realm, Integer port) { + URLProtectionSpace URLProtectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (URLProtectionSpace != null) { + protectionSpaceDao.delete(URLProtectionSpace); + } + } + + public void removeHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + if (protectionSpace != null) { + URLCredential credential = credentialDao.find(username, password, protectionSpace.getId()); + credentialDao.delete(credential); + } + } + + public void setHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) { + URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port); + Long protectionSpaceId; + if (protectionSpace == null) { + protectionSpaceId = protectionSpaceDao.insert(new URLProtectionSpace(null, host, protocol, realm, port)); + } else { + protectionSpaceId = protectionSpace.getId(); + } + + URLCredential credential = credentialDao.find(username, password, protectionSpaceId); + if (credential != null) { + boolean needUpdate = false; + if (!credential.getUsername().equals(username)) { + credential.setUsername(username); + needUpdate = true; + } + if (!credential.getPassword().equals(password)) { + credential.setPassword(password); + needUpdate = true; + } + if (needUpdate) + credentialDao.update(credential); + } else { + credential = new URLCredential(null, username, password, protectionSpaceId); + credential.setId(credentialDao.insert(credential)); + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabaseHandler.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHandler.java similarity index 82% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabaseHandler.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHandler.java index 54ef1e5e..794968ec 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabaseHandler.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHandler.java @@ -1,13 +1,14 @@ -package com.pichillilorenzo.flutter_inappwebview; +package com.pichillilorenzo.flutter_inappwebview.credential_database; import android.os.Build; import android.webkit.WebViewDatabase; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; -import com.pichillilorenzo.flutter_inappwebview.CredentialDatabase.Credential; -import com.pichillilorenzo.flutter_inappwebview.CredentialDatabase.CredentialDatabase; -import com.pichillilorenzo.flutter_inappwebview.CredentialDatabase.ProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview.types.URLProtectionSpace; import java.util.ArrayList; import java.util.HashMap; @@ -17,7 +18,6 @@ import java.util.Map; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; @RequiresApi(api = Build.VERSION_CODES.O) public class CredentialDatabaseHandler implements MethodChannel.MethodCallHandler { @@ -34,15 +34,15 @@ public class CredentialDatabaseHandler implements MethodChannel.MethodCallHandle } @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { + public void onMethodCall(MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { case "getAllAuthCredentials": { List> allCredentials = new ArrayList<>(); - List protectionSpaces = credentialDatabase.protectionSpaceDao.getAll(); - for (ProtectionSpace protectionSpace : protectionSpaces) { + List protectionSpaces = credentialDatabase.protectionSpaceDao.getAll(); + for (URLProtectionSpace protectionSpace : protectionSpaces) { List> credentials = new ArrayList<>(); - for (Credential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.id)) { + for (URLCredential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.getId())) { credentials.add(credential.toMap()); } Map obj = new HashMap<>(); @@ -61,7 +61,7 @@ public class CredentialDatabaseHandler implements MethodChannel.MethodCallHandle Integer port = (Integer) call.argument("port"); List> credentials = new ArrayList<>(); - for (Credential credential : credentialDatabase.getHttpAuthCredentials(host, protocol, realm, port)) { + for (URLCredential credential : credentialDatabase.getHttpAuthCredentials(host, protocol, realm, port)) { credentials.add(credential.toMap()); } result.success(credentials); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHelper.java new file mode 100755 index 00000000..3a0abc11 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/CredentialDatabaseHelper.java @@ -0,0 +1,66 @@ +package com.pichillilorenzo.flutter_inappwebview.credential_database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class CredentialDatabaseHelper extends SQLiteOpenHelper { + + private static final String SQL_CREATE_PROTECTION_SPACE_TABLE = + "CREATE TABLE " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + + URLProtectionSpaceContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " TEXT NOT NULL," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " TEXT," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " TEXT," + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " INTEGER," + + "UNIQUE(" + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + ", " + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + + ")" + + ");"; + + private static final String SQL_CREATE_CREDENTIAL_TABLE = + "CREATE TABLE " + URLCredentialContract.FeedEntry.TABLE_NAME + " (" + + URLCredentialContract.FeedEntry._ID + " INTEGER PRIMARY KEY," + + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " TEXT NOT NULL," + + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " TEXT NOT NULL," + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " INTEGER NOT NULL," + + "UNIQUE(" + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + ", " + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + ", " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + + ")," + + "FOREIGN KEY (" + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + ") REFERENCES " + + URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + URLProtectionSpaceContract.FeedEntry._ID + ") ON DELETE CASCADE" + + ");"; + + private static final String SQL_DELETE_PROTECTION_SPACE_TABLE = + "DROP TABLE IF EXISTS " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME; + + private static final String SQL_DELETE_CREDENTIAL_TABLE = + "DROP TABLE IF EXISTS " + URLCredentialContract.FeedEntry.TABLE_NAME; + + public CredentialDatabaseHelper(Context context) { + super(context, CredentialDatabase.DATABASE_NAME, null, CredentialDatabase.DATABASE_VERSION); + } + + public void onCreate(SQLiteDatabase db) { + db.execSQL(SQL_CREATE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_CREATE_CREDENTIAL_TABLE); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This database is only a cache for online data, so its upgrade policy is + // to simply to discard the data and start over + db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); + onCreate(db); + } + + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + public void clearAllTables(SQLiteDatabase db) { + db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE); + db.execSQL(SQL_DELETE_CREDENTIAL_TABLE); + onCreate(db); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialContract.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialContract.java similarity index 75% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialContract.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialContract.java index 65bb196b..1b301970 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/CredentialContract.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialContract.java @@ -1,9 +1,9 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; +package com.pichillilorenzo.flutter_inappwebview.credential_database; import android.provider.BaseColumns; -public class CredentialContract { - private CredentialContract() {} +public class URLCredentialContract { + private URLCredentialContract() {} /* Inner class that defines the table contents */ public static class FeedEntry implements BaseColumns { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialDao.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialDao.java new file mode 100755 index 00000000..7f299cac --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLCredentialDao.java @@ -0,0 +1,106 @@ +package com.pichillilorenzo.flutter_inappwebview.credential_database; + +import android.content.ContentValues; +import android.database.Cursor; + +import com.pichillilorenzo.flutter_inappwebview.types.URLCredential; + +import java.util.ArrayList; +import java.util.List; + +public class URLCredentialDao { + + CredentialDatabaseHelper credentialDatabaseHelper; + String[] projection = { + URLCredentialContract.FeedEntry._ID, + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + }; + + public URLCredentialDao(CredentialDatabaseHelper credentialDatabaseHelper) { + this.credentialDatabaseHelper = credentialDatabaseHelper; + } + + public List getAllByProtectionSpaceId(Long protectionSpaceId) { + String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] selectionArgs = {protectionSpaceId.toString()}; + + Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( + URLCredentialContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + List URLCredentials = new ArrayList<>(); + while (cursor.moveToNext()) { + Long id = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID)); + String username = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); + String password = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); + URLCredentials.add(new URLCredential(id, username, password, protectionSpaceId)); + } + cursor.close(); + + return URLCredentials; + } + + public URLCredential find(String username, String password, Long protectionSpaceId) { + String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " = ? AND " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " = ? AND " + + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] selectionArgs = {username, password, protectionSpaceId.toString()}; + + Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query( + URLCredentialContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + URLCredential URLCredential = null; + if (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID)); + String rowUsername = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME)); + String rowPassword = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD)); + URLCredential = new URLCredential(rowId, rowUsername, rowPassword, protectionSpaceId); + } + cursor.close(); + + return URLCredential; + } + + public long insert(URLCredential urlCredential) { + ContentValues credentialValues = new ContentValues(); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID, urlCredential.getProtectionSpaceId()); + + return credentialDatabaseHelper.getWritableDatabase().insert(URLCredentialContract.FeedEntry.TABLE_NAME, null, credentialValues); + } + + public long update(URLCredential urlCredential) { + ContentValues credentialValues = new ContentValues(); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername()); + credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword()); + + String whereClause = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?"; + String[] whereArgs = {urlCredential.getProtectionSpaceId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().update(URLCredentialContract.FeedEntry.TABLE_NAME, credentialValues, whereClause, whereArgs); + } + + public long delete(URLCredential urlCredential) { + String whereClause = URLCredentialContract.FeedEntry._ID + " = ?"; + String[] whereArgs = {urlCredential.getId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().delete(URLCredentialContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); + } + +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceContract.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceContract.java similarity index 74% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceContract.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceContract.java index d124644c..0d1e73d9 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/CredentialDatabase/ProtectionSpaceContract.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceContract.java @@ -1,9 +1,9 @@ -package com.pichillilorenzo.flutter_inappwebview.CredentialDatabase; +package com.pichillilorenzo.flutter_inappwebview.credential_database; import android.provider.BaseColumns; -public class ProtectionSpaceContract { - private ProtectionSpaceContract() {} +public class URLProtectionSpaceContract { + private URLProtectionSpaceContract() {} /* Inner class that defines the table contents */ public static class FeedEntry implements BaseColumns { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceDao.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceDao.java new file mode 100755 index 00000000..d23644cc --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/credential_database/URLProtectionSpaceDao.java @@ -0,0 +1,100 @@ +package com.pichillilorenzo.flutter_inappwebview.credential_database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.pichillilorenzo.flutter_inappwebview.types.URLProtectionSpace; + +import java.util.ArrayList; +import java.util.List; + +public class URLProtectionSpaceDao { + CredentialDatabaseHelper credentialDatabaseHelper; + String[] projection = { + URLProtectionSpaceContract.FeedEntry._ID, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + }; + + public URLProtectionSpaceDao(CredentialDatabaseHelper credentialDatabaseHelper) { + this.credentialDatabaseHelper = credentialDatabaseHelper; + } + + public List getAll() { + SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); + + Cursor cursor = readableDatabase.query( + URLProtectionSpaceContract.FeedEntry.TABLE_NAME, + projection, + null, + null, + null, + null, + null + ); + + List URLProtectionSpaces = new ArrayList<>(); + while (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID)); + String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); + String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); + String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); + Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); + URLProtectionSpaces.add(new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort)); + } + cursor.close(); + + return URLProtectionSpaces; + } + + public URLProtectionSpace find(String host, String protocol, String realm, Integer port) { + SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); + + String selection = URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " = ? AND " + + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " = ?"; + String[] selectionArgs = {host, protocol, realm, port.toString()}; + + Cursor cursor = readableDatabase.query( + URLProtectionSpaceContract.FeedEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + URLProtectionSpace URLProtectionSpace = null; + if (cursor.moveToNext()) { + Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID)); + String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST)); + String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL)); + String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM)); + Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT)); + URLProtectionSpace = new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort); + } + cursor.close(); + + return URLProtectionSpace; + } + + public long insert(URLProtectionSpace URLProtectionSpace) { + ContentValues protectionSpaceValues = new ContentValues(); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, URLProtectionSpace.getHost()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, URLProtectionSpace.getProtocol()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, URLProtectionSpace.getRealm()); + protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT, URLProtectionSpace.getPort()); + + return credentialDatabaseHelper.getWritableDatabase().insert(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, null, protectionSpaceValues); + }; + + public long delete(URLProtectionSpace URLProtectionSpace) { + String whereClause = URLProtectionSpaceContract.FeedEntry._ID + " = ?"; + String[] whereArgs = {URLProtectionSpace.getId().toString()}; + + return credentialDatabaseHelper.getWritableDatabase().delete(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, whereClause, whereArgs); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/ActivityResultListener.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/ActivityResultListener.java new file mode 100644 index 00000000..92665260 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/ActivityResultListener.java @@ -0,0 +1,8 @@ +package com.pichillilorenzo.flutter_inappwebview.in_app_browser; + +import android.content.Intent; + +public interface ActivityResultListener { + /** @return true if the result has been handled. */ + boolean onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java similarity index 71% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserActivity.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java index a4b6ba36..fef5389c 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java @@ -1,8 +1,10 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppBrowser; +package com.pichillilorenzo.flutter_inappwebview.in_app_browser; +import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import android.os.Message; import android.util.Log; @@ -21,13 +23,17 @@ import android.widget.SearchView; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebView; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebViewChromeClient; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebViewOptions; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebViewChromeClient; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebViewOptions; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewMethodHandler; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview.Util; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -35,7 +41,7 @@ import java.util.Map; import io.flutter.plugin.common.MethodChannel; -public class InAppBrowserActivity extends AppCompatActivity { +public class InAppBrowserActivity extends AppCompatActivity implements InAppBrowserDelegate { static final String LOG_TAG = "InAppBrowserActivity"; public MethodChannel channel; @@ -46,11 +52,10 @@ public class InAppBrowserActivity extends AppCompatActivity { public Menu menu; public SearchView searchView; public InAppBrowserOptions options; - public Map headers; public ProgressBar progressBar; public boolean isHidden = false; public String fromActivity; - public List activityResultListeners = new ArrayList<>(); + private List activityResultListeners = new ArrayList<>(); public InAppWebViewMethodHandler methodCallDelegate; @Override @@ -71,7 +76,7 @@ public class InAppBrowserActivity extends AppCompatActivity { webView = findViewById(R.id.webView); webView.windowId = windowId; - webView.inAppBrowserActivity = this; + webView.inAppBrowserDelegate = this; webView.channel = channel; methodCallDelegate = new InAppWebViewMethodHandler(webView); @@ -79,8 +84,8 @@ public class InAppBrowserActivity extends AppCompatActivity { fromActivity = b.getString("fromActivity"); - HashMap optionsMap = (HashMap) b.getSerializable("options"); - HashMap contextMenu = (HashMap) b.getSerializable("contextMenu"); + Map optionsMap = (Map) b.getSerializable("options"); + Map contextMenu = (Map) b.getSerializable("contextMenu"); List> initialUserScripts = (List>) b.getSerializable("initialUserScripts"); options = new InAppBrowserOptions(); @@ -90,7 +95,14 @@ public class InAppBrowserActivity extends AppCompatActivity { webViewOptions.parse(optionsMap); webView.options = webViewOptions; webView.contextMenu = contextMenu; - webView.userScripts = initialUserScripts; + + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map initialUserScript : initialUserScripts) { + userScripts.add(UserScript.fromMap(initialUserScript)); + } + } + webView.userContentController.addUserOnlyScripts(userScripts); actionBar = getSupportActionBar(); @@ -103,22 +115,35 @@ public class InAppBrowserActivity extends AppCompatActivity { resultMsg.sendToTarget(); } } else { - Boolean isData = b.getBoolean("isData"); - if (!isData) { - headers = (HashMap) b.getSerializable("headers"); - String url = b.getString("url"); - webView.loadUrl(url, headers); + String initialFile = b.getString("initialFile"); + Map initialUrlRequest = (Map) b.getSerializable("initialUrlRequest"); + String initialData = b.getString("initialData"); + if (initialFile != null) { + try { + webView.loadFile(initialFile); + } catch (IOException e) { + e.printStackTrace(); + Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e); + return; + } } - else { - String data = b.getString("data"); + else if (initialData != null) { String mimeType = b.getString("mimeType"); String encoding = b.getString("encoding"); String baseUrl = b.getString("baseUrl"); String historyUrl = b.getString("historyUrl"); - webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + webView.loadDataWithBaseURL(baseUrl, initialData, mimeType, encoding, historyUrl); + } + else if (initialUrlRequest != null) { + URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest); + webView.loadUrl(urlRequest); } } + onBrowserCreated(); + } + + public void onBrowserCreated() { Map obj = new HashMap<>(); channel.invokeMethod("onBrowserCreated", obj); } @@ -134,14 +159,14 @@ public class InAppBrowserActivity extends AppCompatActivity { progressBar = findViewById(R.id.progressBar); - if (!options.progressBar) + if (options.hideProgressBar) progressBar.setMax(0); else progressBar.setMax(100); actionBar.setDisplayShowTitleEnabled(!options.hideTitleBar); - if (!options.toolbarTop) + if (options.hideToolbarTop) actionBar.hide(); if (options.toolbarTopBackgroundColor != null && !options.toolbarTopBackgroundColor.isEmpty()) @@ -168,7 +193,7 @@ public class InAppBrowserActivity extends AppCompatActivity { searchView.setQuery(webView.getUrl(), false); - if (options.toolbarTopFixedTitle.isEmpty()) + if (options.toolbarTopFixedTitle == null || options.toolbarTopFixedTitle.isEmpty()) actionBar.setTitle(webView.getTitle()); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @@ -316,8 +341,8 @@ public class InAppBrowserActivity extends AppCompatActivity { show(); } - if (newOptionsMap.get("progressBar") != null && options.progressBar != newOptions.progressBar && progressBar != null) { - if (newOptions.progressBar) + if (newOptionsMap.get("hideProgressBar") != null && options.hideProgressBar != newOptions.hideProgressBar && progressBar != null) { + if (newOptions.hideProgressBar) progressBar.setMax(0); else progressBar.setMax(100); @@ -326,17 +351,18 @@ public class InAppBrowserActivity extends AppCompatActivity { if (newOptionsMap.get("hideTitleBar") != null && options.hideTitleBar != newOptions.hideTitleBar) actionBar.setDisplayShowTitleEnabled(!newOptions.hideTitleBar); - if (newOptionsMap.get("toolbarTop") != null && options.toolbarTop != newOptions.toolbarTop) { - if (!newOptions.toolbarTop) + if (newOptionsMap.get("hideToolbarTop") != null && options.hideToolbarTop != newOptions.hideToolbarTop) { + if (newOptions.hideToolbarTop) actionBar.hide(); else actionBar.show(); } - if (newOptionsMap.get("toolbarTopBackgroundColor") != null && options.toolbarTopBackgroundColor != newOptions.toolbarTopBackgroundColor && !newOptions.toolbarTopBackgroundColor.isEmpty()) + if (newOptionsMap.get("toolbarTopBackgroundColor") != null && !Util.objEquals(options.toolbarTopBackgroundColor, newOptions.toolbarTopBackgroundColor) && + !newOptions.toolbarTopBackgroundColor.isEmpty()) actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(newOptions.toolbarTopBackgroundColor))); - if (newOptionsMap.get("toolbarTopFixedTitle") != null && options.toolbarTopFixedTitle != newOptions.toolbarTopFixedTitle && !newOptions.toolbarTopFixedTitle.isEmpty()) + if (newOptionsMap.get("toolbarTopFixedTitle") != null && !Util.objEquals(options.toolbarTopFixedTitle, newOptions.toolbarTopFixedTitle) && !newOptions.toolbarTopFixedTitle.isEmpty()) actionBar.setTitle(newOptions.toolbarTopFixedTitle); if (newOptionsMap.get("hideUrlBar") != null && options.hideUrlBar != newOptions.hideUrlBar) { @@ -359,6 +385,69 @@ public class InAppBrowserActivity extends AppCompatActivity { return optionsMap; } + @Override + public Activity getActivity() { + return this; + } + + @Override + public void didChangeTitle(String title) { + if (options.toolbarTopFixedTitle == null || options.toolbarTopFixedTitle.isEmpty()) { + actionBar.setTitle(title); + } + } + + @Override + public void didStartNavigation(String url) { + progressBar.setProgress(0); + searchView.setQuery(url, false); + } + + @Override + public void didUpdateVisitedHistory(String url) { + searchView.setQuery(url, false); + } + + @Override + public void didFinishNavigation(String url) { + searchView.setQuery(url, false); + progressBar.setProgress(0); + } + + @Override + public void didFailNavigation(String url, int errorCode, String description) { + progressBar.setProgress(0); + } + + @Override + public void didChangeProgress(int progress) { + progressBar.setVisibility(View.VISIBLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + progressBar.setProgress(progress, true); + } else { + progressBar.setProgress(progress); + } + if (progress == 100) { + progressBar.setVisibility(View.GONE); + } + } + + public List getActivityResultListeners() { + return activityResultListeners; + } + + @Override + protected void onActivityResult (int requestCode, + int resultCode, + Intent data) { + for (ActivityResultListener listener : activityResultListeners) { + if (listener.onActivityResult(requestCode, resultCode, data)) { + return; + } + } + super.onActivityResult(requestCode, resultCode, data); + } + public void dispose() { channel.setMethodCallHandler(null); activityResultListeners.clear(); @@ -385,26 +474,9 @@ public class InAppBrowserActivity extends AppCompatActivity { } } - @Override - protected void onActivityResult (int requestCode, - int resultCode, - Intent data) { - for (ActivityResultListener listener : activityResultListeners) { - if (listener.onActivityResult(requestCode, resultCode, data)) { - return; - } - } - super.onActivityResult(requestCode, resultCode, data); - } - @Override public void onDestroy() { dispose(); super.onDestroy(); } - - public interface ActivityResultListener { - /** @return true if the result has been handled. */ - boolean onActivityResult(int requestCode, int resultCode, Intent data); - } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserDelegate.java new file mode 100644 index 00000000..a7bb8b2a --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserDelegate.java @@ -0,0 +1,16 @@ +package com.pichillilorenzo.flutter_inappwebview.in_app_browser; + +import android.app.Activity; + +import java.util.List; + +public interface InAppBrowserDelegate { + Activity getActivity(); + List getActivityResultListeners(); + void didChangeTitle(String title); + void didStartNavigation(String url); + void didUpdateVisitedHistory(String url); + void didFinishNavigation(String url); + void didFailNavigation(String url, int errorCode, String description); + void didChangeProgress(int progress); +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java similarity index 71% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowserManager.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java index acefff4a..5b427bf1 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowserManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java @@ -19,7 +19,7 @@ * */ -package com.pichillilorenzo.flutter_inappwebview; +package com.pichillilorenzo.flutter_inappwebview.in_app_browser; import android.app.Activity; import android.content.Intent; @@ -32,12 +32,10 @@ import android.os.Bundle; import android.webkit.MimeTypeMap; import android.util.Log; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview.Shared; -import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -62,49 +60,42 @@ public class InAppBrowserManager implements MethodChannel.MethodCallHandler { @Override public void onMethodCall(final MethodCall call, final Result result) { final Activity activity = Shared.activity; - final String uuid = (String) call.argument("uuid"); switch (call.method) { - case "openUrl": + case "openUrlRequest": { - String url = (String) call.argument("url"); - HashMap options = (HashMap) call.argument("options"); - Map headers = (Map) call.argument("headers"); - HashMap contextMenu = (HashMap) call.argument("contextMenu"); + String uuid = (String) call.argument("uuid"); + Map urlRequest = (Map) call.argument("urlRequest"); + Map options = (Map) call.argument("options"); + Map contextMenu = (Map) call.argument("contextMenu"); Integer windowId = (Integer) call.argument("windowId"); List> initialUserScripts = (List>) call.argument("initialUserScripts"); - openUrl(activity, uuid, url, options, headers, contextMenu, windowId, initialUserScripts); + openUrlRequest(activity, uuid, urlRequest, options, contextMenu, windowId, initialUserScripts); } result.success(true); break; case "openFile": { - String url = (String) call.argument("url"); - try { - url = Util.getUrlAsset(url); - } catch (IOException e) { - e.printStackTrace(); - result.error(LOG_TAG, url + " asset file cannot be found!", e); - return; - } - HashMap options = (HashMap) call.argument("options"); - Map headers = (Map) call.argument("headers"); - HashMap contextMenu = (HashMap) call.argument("contextMenu"); + String uuid = (String) call.argument("uuid"); + String assetFilePath = (String) call.argument("assetFilePath"); + Map options = (Map) call.argument("options"); + Map contextMenu = (Map) call.argument("contextMenu"); Integer windowId = (Integer) call.argument("windowId"); List> initialUserScripts = (List>) call.argument("initialUserScripts"); - openUrl(activity, uuid, url, options, headers, contextMenu, windowId, initialUserScripts); + openFile(activity, uuid, assetFilePath, options, contextMenu, windowId, initialUserScripts); } result.success(true); break; case "openData": { - HashMap options = (HashMap) call.argument("options"); + String uuid = (String) call.argument("uuid"); + Map options = (Map) call.argument("options"); String data = (String) call.argument("data"); String mimeType = (String) call.argument("mimeType"); String encoding = (String) call.argument("encoding"); String baseUrl = (String) call.argument("baseUrl"); String historyUrl = (String) call.argument("historyUrl"); - HashMap contextMenu = (HashMap) call.argument("contextMenu"); + Map contextMenu = (Map) call.argument("contextMenu"); Integer windowId = (Integer) call.argument("windowId"); List> initialUserScripts = (List>) call.argument("initialUserScripts"); openData(activity, uuid, options, data, mimeType, encoding, baseUrl, historyUrl, contextMenu, windowId, initialUserScripts); @@ -198,32 +189,42 @@ public class InAppBrowserManager implements MethodChannel.MethodCallHandler { } } - public void openUrl(Activity activity, String uuid, String url, HashMap options, Map headers, - HashMap contextMenu, Integer windowId, List> initialUserScripts) { + public void openUrlRequest(Activity activity, String uuid, Map urlRequest, Map options, + Map contextMenu, Integer windowId, List> initialUserScripts) { Bundle extras = new Bundle(); extras.putString("fromActivity", activity.getClass().getName()); - extras.putString("url", url); - extras.putBoolean("isData", false); + extras.putSerializable("initialUrlRequest", (Serializable) urlRequest); extras.putString("uuid", uuid); - extras.putSerializable("options", options); - extras.putSerializable("headers", (Serializable) headers); + extras.putSerializable("options", (Serializable) options); extras.putSerializable("contextMenu", (Serializable) contextMenu); extras.putInt("windowId", windowId != null ? windowId : -1); extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); startInAppBrowserActivity(activity, extras); } - public void openData(Activity activity, String uuid, HashMap options, String data, String mimeType, String encoding, - String baseUrl, String historyUrl, HashMap contextMenu, Integer windowId, List> initialUserScripts) { + public void openFile(Activity activity, String uuid, String assetFilePath, Map options, + Map contextMenu, Integer windowId, List> initialUserScripts) { Bundle extras = new Bundle(); - extras.putBoolean("isData", true); + extras.putString("fromActivity", activity.getClass().getName()); + extras.putString("initialFile", assetFilePath); extras.putString("uuid", uuid); - extras.putSerializable("options", options); - extras.putString("data", data); - extras.putString("mimeType", mimeType); - extras.putString("encoding", encoding); - extras.putString("baseUrl", baseUrl); - extras.putString("historyUrl", historyUrl); + extras.putSerializable("options", (Serializable) options); + extras.putSerializable("contextMenu", (Serializable) contextMenu); + extras.putInt("windowId", windowId != null ? windowId : -1); + extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); + startInAppBrowserActivity(activity, extras); + } + + public void openData(Activity activity, String uuid, Map options, String data, String mimeType, String encoding, + String baseUrl, String historyUrl, Map contextMenu, Integer windowId, List> initialUserScripts) { + Bundle extras = new Bundle(); + extras.putString("uuid", uuid); + extras.putSerializable("options", (Serializable) options); + extras.putString("initialData", data); + extras.putString("initialMimeType", mimeType); + extras.putString("initialEncoding", encoding); + extras.putString("initialBaseUrl", baseUrl); + extras.putString("initialHistoryUrl", historyUrl); extras.putSerializable("contextMenu", (Serializable) contextMenu); extras.putInt("windowId", windowId != null ? windowId : -1); extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java similarity index 69% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserOptions.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java index 0c09dd26..96bac5e5 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppBrowser/InAppBrowserOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserOptions.java @@ -1,6 +1,9 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppBrowser; +package com.pichillilorenzo.flutter_inappwebview.in_app_browser; + +import androidx.annotation.Nullable; import com.pichillilorenzo.flutter_inappwebview.Options; +import com.pichillilorenzo.flutter_inappwebview.R; import java.util.HashMap; import java.util.Map; @@ -10,14 +13,16 @@ public class InAppBrowserOptions implements Options { public static final String LOG_TAG = "InAppBrowserOptions"; public Boolean hidden = false; - public Boolean toolbarTop = true; - public String toolbarTopBackgroundColor = ""; - public String toolbarTopFixedTitle = ""; + public Boolean hideToolbarTop = false; + @Nullable + public String toolbarTopBackgroundColor = null; + @Nullable + public String toolbarTopFixedTitle; public Boolean hideUrlBar = false; + public Boolean hideProgressBar = false; public Boolean hideTitleBar = false; public Boolean closeOnCannotGoBack = true; - public Boolean progressBar = true; @Override public InAppBrowserOptions parse(Map options) { @@ -32,8 +37,8 @@ public class InAppBrowserOptions implements Options { case "hidden": hidden = (Boolean) value; break; - case "toolbarTop": - toolbarTop = (Boolean) value; + case "hideToolbarTop": + hideToolbarTop = (Boolean) value; break; case "toolbarTopBackgroundColor": toolbarTopBackgroundColor = (String) value; @@ -50,8 +55,8 @@ public class InAppBrowserOptions implements Options { case "closeOnCannotGoBack": closeOnCannotGoBack = (Boolean) value; break; - case "progressBar": - progressBar = (Boolean) value; + case "hideProgressBar": + hideProgressBar = (Boolean) value; break; } } @@ -63,19 +68,22 @@ public class InAppBrowserOptions implements Options { public Map toMap() { Map options = new HashMap<>(); options.put("hidden", hidden); - options.put("toolbarTop", toolbarTop); + options.put("hideToolbarTop", hideToolbarTop); options.put("toolbarTopBackgroundColor", toolbarTopBackgroundColor); options.put("toolbarTopFixedTitle", toolbarTopFixedTitle); options.put("hideUrlBar", hideUrlBar); options.put("hideTitleBar", hideTitleBar); options.put("closeOnCannotGoBack", closeOnCannotGoBack); - options.put("progressBar", progressBar); + options.put("hideProgressBar", hideProgressBar); return options; } @Override public Map getRealOptions(InAppBrowserActivity inAppBrowserActivity) { Map realOptions = toMap(); + realOptions.put("hideToolbarTop", inAppBrowserActivity.actionBar.isShowing()); + realOptions.put("hideUrlBar", inAppBrowserActivity.menu.findItem(R.id.menu_search).isVisible()); + realOptions.put("hideProgressBar", inAppBrowserActivity.progressBar.getMax() == 0); return realOptions; } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ContextMenuOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java similarity index 94% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ContextMenuOptions.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java index cb42253c..9736643f 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ContextMenuOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ContextMenuOptions.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import com.pichillilorenzo.flutter_inappwebview.Options; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/DisplayListenerProxy.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/DisplayListenerProxy.java similarity index 98% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/DisplayListenerProxy.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/DisplayListenerProxy.java index cdc7b93e..50bd2e4b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/DisplayListenerProxy.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/DisplayListenerProxy.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import static android.hardware.display.DisplayManager.DisplayListener; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java similarity index 73% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebView.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java index cab5f196..5ee0019a 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebView.java @@ -1,7 +1,8 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.content.Context; import android.hardware.display.DisplayManager; +import android.os.Build; import android.os.Message; import android.util.Log; import android.view.View; @@ -10,11 +11,18 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + import com.pichillilorenzo.flutter_inappwebview.InAppWebViewMethodHandler; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview.types.UserScript; import com.pichillilorenzo.flutter_inappwebview.Util; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,10 +46,9 @@ public class FlutterWebView implements PlatformView { DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); - String initialUrl = (String) params.get("initialUrl"); + Map initialUrlRequest = (Map) params.get("initialUrlRequest"); final String initialFile = (String) params.get("initialFile"); final Map initialData = (Map) params.get("initialData"); - final Map initialHeaders = (Map) params.get("initialHeaders"); Map initialOptions = (Map) params.get("initialOptions"); Map contextMenu = (Map) params.get("contextMenu"); Integer windowId = (Integer) params.get("windowId"); @@ -57,7 +64,14 @@ public class FlutterWebView implements PlatformView { "- See the official wiki here: https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects\n\n\n"); } - webView = new InAppWebView(context, this, id, windowId, options, contextMenu, containerView, initialUserScripts); + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map initialUserScript : initialUserScripts) { + userScripts.add(UserScript.fromMap(initialUserScript)); + } + } + + webView = new InAppWebView(context, channel, id, windowId, options, contextMenu, containerView, userScripts); displayListenerProxy.onPostWebViewInitialization(displayManager); methodCallDelegate = new InAppWebViewMethodHandler(webView); @@ -74,15 +88,14 @@ public class FlutterWebView implements PlatformView { } else { if (initialFile != null) { try { - initialUrl = Util.getUrlAsset(initialFile); + webView.loadFile(initialFile); } catch (IOException e) { e.printStackTrace(); Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e); return; } } - - if (initialData != null) { + else if (initialData != null) { String data = initialData.get("data"); String mimeType = initialData.get("mimeType"); String encoding = initialData.get("encoding"); @@ -90,8 +103,9 @@ public class FlutterWebView implements PlatformView { String historyUrl = initialData.get("historyUrl"); webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } - else { - webView.loadUrl(initialUrl, initialHeaders); + else if (initialUrlRequest != null) { + URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest); + webView.loadUrl(urlRequest); } } @@ -114,13 +128,20 @@ public class FlutterWebView implements PlatformView { methodCallDelegate = null; } if (webView != null) { - webView.inAppWebViewChromeClient.dispose(); - webView.inAppWebViewClient.dispose(); - webView.javaScriptBridgeInterface.dispose(); + webView.removeJavascriptInterface(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { + WebViewCompat.setWebViewRenderProcessClient(webView, null); + } webView.setWebChromeClient(new WebChromeClient()); webView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { + if (webView.inAppWebViewRenderProcessClient != null) { + webView.inAppWebViewRenderProcessClient.dispose(); + } + webView.inAppWebViewChromeClient.dispose(); + webView.inAppWebViewClient.dispose(); + webView.javaScriptBridgeInterface.dispose(); webView.dispose(); webView.destroy(); webView = null; @@ -134,13 +155,13 @@ public class FlutterWebView implements PlatformView { @Override public void onInputConnectionLocked() { - if (webView != null && webView.inAppBrowserActivity == null) + if (webView != null && webView.inAppBrowserDelegate == null) webView.lockInputConnection(); } @Override public void onInputConnectionUnlocked() { - if (webView != null && webView.inAppBrowserActivity == null) + if (webView != null && webView.inAppBrowserDelegate == null) webView.unlockInputConnection(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebViewFactory.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebViewFactory.java similarity index 86% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebViewFactory.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebViewFactory.java index 76b31026..4d85ef86 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/FlutterWebViewFactory.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/FlutterWebViewFactory.java @@ -1,10 +1,8 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.content.Context; import android.view.View; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.FlutterWebView; - import java.util.HashMap; import io.flutter.plugin.common.BinaryMessenger; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/HeadlessInAppWebViewManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/HeadlessInAppWebViewManager.java similarity index 95% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/HeadlessInAppWebViewManager.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/HeadlessInAppWebViewManager.java index cf82bb30..856d522b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/HeadlessInAppWebViewManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/HeadlessInAppWebViewManager.java @@ -19,11 +19,11 @@ * */ -package com.pichillilorenzo.flutter_inappwebview; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.app.Activity; -import com.pichillilorenzo.flutter_inappwebview.InAppWebView.FlutterWebView; +import com.pichillilorenzo.flutter_inappwebview.Shared; import java.util.HashMap; import java.util.Map; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java similarity index 58% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java index c491728c..2c4d1b79 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java @@ -1,13 +1,13 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Point; -import android.net.http.SslCertificate; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -43,36 +43,46 @@ import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; -import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlocker; -import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlockerAction; -import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlockerHandler; -import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlockerTrigger; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlocker; +import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerAction; +import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerHandler; +import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerTrigger; +import com.pichillilorenzo.flutter_inappwebview.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserDelegate; import com.pichillilorenzo.flutter_inappwebview.JavaScriptBridgeInterface; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview.types.UserScript; import com.pichillilorenzo.flutter_inappwebview.Util; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.ConsoleLogJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.InterceptAjaxRequestJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.InterceptFetchRequestJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.OnLoadResourceJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.OnWindowBlurEventJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.OnWindowFocusEventJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PluginScriptsUtil; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PrintJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PromisePolyfillJS; -import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; @@ -80,19 +90,21 @@ import io.flutter.plugin.common.MethodChannel; import okhttp3.OkHttpClient; import static android.content.Context.INPUT_METHOD_SERVICE; -import static com.pichillilorenzo.flutter_inappwebview.InAppWebView.PreferredContentModeOptionType.fromValue; +import static com.pichillilorenzo.flutter_inappwebview.types.PreferredContentModeOptionType.fromValue; final public class InAppWebView extends InputAwareWebView { static final String LOG_TAG = "InAppWebView"; - public InAppBrowserActivity inAppBrowserActivity; - public FlutterWebView flutterWebView; + @Nullable + public InAppBrowserDelegate inAppBrowserDelegate; public MethodChannel channel; public Object id; + @Nullable public Integer windowId; public InAppWebViewClient inAppWebViewClient; public InAppWebViewChromeClient inAppWebViewChromeClient; + @Nullable public InAppWebViewRenderProcessClient inAppWebViewRenderProcessClient; public JavaScriptBridgeInterface javaScriptBridgeInterface; public InAppWebViewOptions options; @@ -102,12 +114,14 @@ final public class InAppWebView extends InputAwareWebView { int okHttpClientCacheSize = 10 * 1024 * 1024; // 10MB public ContentBlockerHandler contentBlockerHandler = new ContentBlockerHandler(); public Pattern regexToCancelSubFramesLoadingCompiled; + @Nullable public GestureDetector gestureDetector = null; + @Nullable public LinearLayout floatingContextMenu = null; + @Nullable public Map contextMenu = null; public Handler headlessHandler = new Handler(Looper.getMainLooper()); static Handler mHandler = new Handler(); - public List> userScripts = new ArrayList<>(); public Runnable checkScrollStoppedTask; public int initialPositionScrollStoppedTask; @@ -116,571 +130,10 @@ final public class InAppWebView extends InputAwareWebView { public Runnable checkContextMenuShouldBeClosedTask; public int newCheckContextMenuShouldBeClosedTaskTask = 100; // ms - public Set userScriptsContentWorlds = new HashSet() {{ - add("page"); - }}; + public UserContentController userContentController = new UserContentController(); - public Map callAsyncJavaScriptResults = new HashMap<>(); - - static final String pluginScriptsWrapperJS = "(function(){" + - " if (window." + JavaScriptBridgeInterface.name + " == null || window." + JavaScriptBridgeInterface.name + "._pluginScriptsLoaded == null || !window." + JavaScriptBridgeInterface.name + "._pluginScriptsLoaded) {" + - " $PLACEHOLDER_VALUE" + - " window." + JavaScriptBridgeInterface.name + "._pluginScriptsLoaded = true;" + - " }" + - "})();"; - - static final String userScriptsAtDocumentStartWrapperJS = "if (window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentStartLoaded == null || !window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentStartLoaded) {" + - " $PLACEHOLDER_VALUE" + - " window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentStartLoaded = true;" + - "}"; - - static final String userScriptsAtDocumentEndWrapperJS = "if (window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentEndLoaded == null || !window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentEndLoaded) {" + - " $PLACEHOLDER_VALUE" + - " window." + JavaScriptBridgeInterface.name + "._userScriptsAtDocumentEndLoaded = true;" + - "}"; - - static final String contentWorldWrapperJS = "(function() {" + - " var iframeId = '" + JavaScriptBridgeInterface.name + "_$CONTENT_WORLD_NAME';" + - " var iframe = document.getElementById(iframeId);" + - " if (iframe == null) {" + - " iframe = document.createElement('iframe');" + - " iframe.id = iframeId;" + - " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + - " document.body.append(iframe);" + - " }" + - " var script = iframe.contentWindow.document.createElement('script');" + - " var sourceEncoded = $JSON_SOURCE_ENCODED;" + - " script.innerHTML = sourceEncoded.source;" + - " iframe.contentWindow.document.body.append(script);" + - "})();"; - - static final String documentReadyWrapperJS = "if (document.readyState === 'interactive' || document.readyState === 'complete') { " + - " $PLACEHOLDER_VALUE" + - "} else {" + - " document.addEventListener('DOMContentLoaded', function() {" + - " $PLACEHOLDER_VALUE" + - " });" + - "}"; - - static final String consoleLogJS = "(function(console) {" + - " var oldLogs = {" + - " 'log': console.log," + - " 'debug': console.debug," + - " 'error': console.error," + - " 'info': console.info," + - " 'warn': console.warn" + - " };" + - " for (var k in oldLogs) {" + - " (function(oldLog) {" + - " console[oldLog] = function() {" + - " var message = '';" + - " for (var i in arguments) {" + - " if (message == '') {" + - " message += arguments[i];" + - " }" + - " else {" + - " message += ' ' + arguments[i];" + - " }" + - " }" + - " oldLogs[oldLog].call(console, message);" + - " }" + - " })(k);" + - " }" + - "})(window.console);"; - - static final String printJS = "window.print = function() {" + - " if (window.top == null || window.top === window) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onPrint', window.location.href);" + - " } else {" + - " window.top.print();" + - " }" + - "};"; - - static final String platformReadyJS = "(function() {" + - " if ((window.top == null || window.top === window) && window." + JavaScriptBridgeInterface.name + "._platformReady == null) {" + - " window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));" + - " window." + JavaScriptBridgeInterface.name + "._platformReady = true;" + - " }" + - "})();"; - - static final String variableForOnLoadResourceJS = "_flutter_inappwebview_useOnLoadResource"; - static final String enableVariableForOnLoadResourceJS = "window." + variableForOnLoadResourceJS + " = $PLACEHOLDER_VALUE;"; - - static final String resourceObserverJS = "(function() {" + - " var observer = new PerformanceObserver(function(list) {" + - " list.getEntries().forEach(function(entry) {" + - " if (window." + variableForOnLoadResourceJS + " == null || window." + variableForOnLoadResourceJS + " == true) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onLoadResource', entry);" + - " }" + - " });" + - " });" + - " observer.observe({entryTypes: ['resource']});" + - "})();"; - - static final String variableForShouldInterceptAjaxRequestJS = "_flutter_inappwebview_useShouldInterceptAjaxRequest"; - static final String enableVariableForShouldInterceptAjaxRequestJS = "window." + variableForShouldInterceptAjaxRequestJS + " = $PLACEHOLDER_VALUE;"; - - static final String interceptAjaxRequestsJS = "(function(ajax) {" + - " var send = ajax.prototype.send;" + - " var open = ajax.prototype.open;" + - " var setRequestHeader = ajax.prototype.setRequestHeader;" + - " ajax.prototype._flutter_inappwebview_url = null;" + - " ajax.prototype._flutter_inappwebview_method = null;" + - " ajax.prototype._flutter_inappwebview_isAsync = null;" + - " ajax.prototype._flutter_inappwebview_user = null;" + - " ajax.prototype._flutter_inappwebview_password = null;" + - " ajax.prototype._flutter_inappwebview_password = null;" + - " ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false;" + - " ajax.prototype._flutter_inappwebview_request_headers = {};" + - " function convertRequestResponse(request, callback) {" + - " if (request.response != null && request.responseType != null) {" + - " switch (request.responseType) {" + - " case 'arraybuffer':" + - " callback(new Uint8Array(request.response));" + - " return;" + - " case 'blob':" + - " const reader = new FileReader();" + - " reader.addEventListener('loadend', function() { " + - " callback(new Uint8Array(reader.result));" + - " });" + - " reader.readAsArrayBuffer(blob);" + - " return;" + - " case 'document':" + - " callback(request.response.documentElement.outerHTML);" + - " return;" + - " case 'json':" + - " callback(request.response);" + - " return;" + - " };" + - " }" + - " callback(null);" + - " };" + - " ajax.prototype.open = function(method, url, isAsync, user, password) {" + - " isAsync = (isAsync != null) ? isAsync : true;" + - " this._flutter_inappwebview_url = url;" + - " this._flutter_inappwebview_method = method;" + - " this._flutter_inappwebview_isAsync = isAsync;" + - " this._flutter_inappwebview_user = user;" + - " this._flutter_inappwebview_password = password;" + - " this._flutter_inappwebview_request_headers = {};" + - " open.call(this, method, url, isAsync, user, password);" + - " };" + - " ajax.prototype.setRequestHeader = function(header, value) {" + - " this._flutter_inappwebview_request_headers[header] = value;" + - " setRequestHeader.call(this, header, value);" + - " };" + - " function handleEvent(e) {" + - " var self = this;" + - " var w = (window.top == null || window.top === window) ? window : window.top;" + - " if (w." + variableForShouldInterceptAjaxRequestJS + " == null || w." + variableForShouldInterceptAjaxRequestJS + " == true) {" + - " var headers = this.getAllResponseHeaders();" + - " var responseHeaders = {};" + - " if (headers != null) {" + - " var arr = headers.trim().split(/[\\r\\n]+/);" + - " arr.forEach(function (line) {" + - " var parts = line.split(': ');" + - " var header = parts.shift();" + - " var value = parts.join(': ');" + - " responseHeaders[header] = value;" + - " });" + - " }" + - " convertRequestResponse(this, function(response) {" + - " var ajaxRequest = {" + - " method: self._flutter_inappwebview_method," + - " url: self._flutter_inappwebview_url," + - " isAsync: self._flutter_inappwebview_isAsync," + - " user: self._flutter_inappwebview_user," + - " password: self._flutter_inappwebview_password," + - " withCredentials: self.withCredentials," + - " headers: self._flutter_inappwebview_request_headers," + - " readyState: self.readyState," + - " status: self.status," + - " responseURL: self.responseURL," + - " responseType: self.responseType," + - " response: response," + - " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + - " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + - " statusText: self.statusText," + - " responseHeaders, responseHeaders," + - " event: {" + - " type: e.type," + - " loaded: e.loaded," + - " lengthComputable: e.lengthComputable," + - " total: e.total" + - " }" + - " };" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onAjaxProgress', ajaxRequest).then(function(result) {" + - " if (result != null) {" + - " switch (result) {" + - " case 0:" + - " self.abort();" + - " return;" + - " };" + - " }" + - " });" + - " });" + - " }" + - " };" + - " ajax.prototype.send = function(data) {" + - " var self = this;" + - " var w = (window.top == null || window.top === window) ? window : window.top;" + - " if (w." + variableForShouldInterceptAjaxRequestJS + " == null || w." + variableForShouldInterceptAjaxRequestJS + " == true) {" + - " if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) {" + - " this._flutter_inappwebview_already_onreadystatechange_wrapped = true;" + - " var onreadystatechange = this.onreadystatechange;" + - " this.onreadystatechange = function() {" + - " var w = (window.top == null || window.top === window) ? window : window.top;" + - " if (w." + variableForShouldInterceptAjaxRequestJS + " == null || w." + variableForShouldInterceptAjaxRequestJS + " == true) {" + - " var headers = this.getAllResponseHeaders();" + - " var responseHeaders = {};" + - " if (headers != null) {" + - " var arr = headers.trim().split(/[\\r\\n]+/);" + - " arr.forEach(function (line) {" + - " var parts = line.split(': ');" + - " var header = parts.shift();" + - " var value = parts.join(': ');" + - " responseHeaders[header] = value;" + - " });" + - " }" + - " convertRequestResponse(this, function(response) {" + - " var ajaxRequest = {" + - " method: self._flutter_inappwebview_method," + - " url: self._flutter_inappwebview_url," + - " isAsync: self._flutter_inappwebview_isAsync," + - " user: self._flutter_inappwebview_user," + - " password: self._flutter_inappwebview_password," + - " withCredentials: self.withCredentials," + - " headers: self._flutter_inappwebview_request_headers," + - " readyState: self.readyState," + - " status: self.status," + - " responseURL: self.responseURL," + - " responseType: self.responseType," + - " response: response," + - " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + - " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + - " statusText: self.statusText," + - " responseHeaders: responseHeaders" + - " };" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) {" + - " if (result != null) {" + - " switch (result) {" + - " case 0:" + - " self.abort();" + - " return;" + - " };" + - " }" + - " if (onreadystatechange != null) {" + - " onreadystatechange();" + - " }" + - " });" + - " });" + - " } else if (onreadystatechange != null) {" + - " onreadystatechange();" + - " }" + - " };" + - " }" + - " this.addEventListener('loadstart', handleEvent);" + - " this.addEventListener('load', handleEvent);" + - " this.addEventListener('loadend', handleEvent);" + - " this.addEventListener('progress', handleEvent);" + - " this.addEventListener('error', handleEvent);" + - " this.addEventListener('abort', handleEvent);" + - " this.addEventListener('timeout', handleEvent);" + - " var ajaxRequest = {" + - " data: data," + - " method: this._flutter_inappwebview_method," + - " url: this._flutter_inappwebview_url," + - " isAsync: this._flutter_inappwebview_isAsync," + - " user: this._flutter_inappwebview_user," + - " password: this._flutter_inappwebview_password," + - " withCredentials: this.withCredentials," + - " headers: this._flutter_inappwebview_request_headers," + - " responseType: this.responseType" + - " };" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {" + - " if (result != null) {" + - " switch (result.action) {" + - " case 0:" + - " self.abort();" + - " return;" + - " };" + - " data = result.data;" + - " self.withCredentials = result.withCredentials;" + - " if (result.responseType != null) {" + - " self.responseType = result.responseType;" + - " };" + - " for (var header in result.headers) {" + - " var value = result.headers[header];" + - " var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header];" + - " if (flutter_inappwebview_value == null) {" + - " self._flutter_inappwebview_request_headers[header] = value;" + - " } else {" + - " self._flutter_inappwebview_request_headers[header] += ', ' + value;" + - " }" + - " setRequestHeader.call(self, header, value);" + - " };" + - " if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) {" + - " self.abort();" + - " self.open(result.method, result.url, result.isAsync, result.user, result.password);" + - " return;" + - " }" + - " }" + - " send.call(self, data);" + - " });" + - " } else {" + - " send.call(this, data);" + - " }" + - " };" + - "})(window.XMLHttpRequest);"; - - static final String variableForShouldInterceptFetchRequestsJS = "_flutter_inappwebview_useShouldInterceptFetchRequest"; - static final String enableVariableForShouldInterceptFetchRequestsJS = "window." + variableForShouldInterceptFetchRequestsJS + " = $PLACEHOLDER_VALUE;"; - - static final String interceptFetchRequestsJS = "(function(fetch) {" + - " if (fetch == null) {" + - " return;" + - " }" + - " function convertHeadersToJson(headers) {" + - " var headersObj = {};" + - " for (var header of headers.keys()) {" + - " var value = headers.get(header);" + - " headersObj[header] = value;" + - " }" + - " return headersObj;" + - " }" + - " function convertJsonToHeaders(headersJson) {" + - " return new Headers(headersJson);" + - " }" + - " function convertBodyToArray(body) {" + - " return new Response(body).arrayBuffer().then(function(arrayBuffer) {" + - " var arr = Array.from(new Uint8Array(arrayBuffer));" + - " return arr;" + - " })" + - " }" + - " function convertArrayIntBodyToUint8Array(arrayIntBody) {" + - " return new Uint8Array(arrayIntBody);" + - " }" + - " function convertCredentialsToJson(credentials) {" + - " var credentialsObj = {};" + - " if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" + - " credentialsObj.type = credentials.type;" + - " credentialsObj.id = credentials.id;" + - " credentialsObj.name = credentials.name;" + - " credentialsObj.protocol = credentials.protocol;" + - " credentialsObj.provider = credentials.provider;" + - " credentialsObj.iconURL = credentials.iconURL;" + - " } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" + - " credentialsObj.type = credentials.type;" + - " credentialsObj.id = credentials.id;" + - " credentialsObj.name = credentials.name;" + - " credentialsObj.password = credentials.password;" + - " credentialsObj.iconURL = credentials.iconURL;" + - " } else {" + - " credentialsObj.type = 'default';" + - " credentialsObj.value = credentials;" + - " }" + - " }" + - " function convertJsonToCredential(credentialsJson) {" + - " var credentials;" + - " if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" + - " credentials = new FederatedCredential({" + - " id: credentialsJson.id," + - " name: credentialsJson.name," + - " protocol: credentialsJson.protocol," + - " provider: credentialsJson.provider," + - " iconURL: credentialsJson.iconURL" + - " });" + - " } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" + - " credentials = new PasswordCredential({" + - " id: credentialsJson.id," + - " name: credentialsJson.name," + - " password: credentialsJson.password," + - " iconURL: credentialsJson.iconURL" + - " });" + - " } else {" + - " credentials = credentialsJson;" + - " }" + - " return credentials;" + - " }" + - " window.fetch = async function(resource, init) {" + - " var w = (window.top == null || window.top === window) ? window : window.top;" + - " if (w." + variableForShouldInterceptFetchRequestsJS + " == null || w." + variableForShouldInterceptFetchRequestsJS + " == true) {" + - " var fetchRequest = {" + - " url: null," + - " method: null," + - " headers: null," + - " body: null," + - " mode: null," + - " credentials: null," + - " cache: null," + - " redirect: null," + - " referrer: null," + - " referrerPolicy: null," + - " integrity: null," + - " keepalive: null" + - " };" + - " if (resource instanceof Request) {" + - " fetchRequest.url = resource.url;" + - " fetchRequest.method = resource.method;" + - " fetchRequest.headers = resource.headers;" + - " fetchRequest.body = resource.body;" + - " fetchRequest.mode = resource.mode;" + - " fetchRequest.credentials = resource.credentials;" + - " fetchRequest.cache = resource.cache;" + - " fetchRequest.redirect = resource.redirect;" + - " fetchRequest.referrer = resource.referrer;" + - " fetchRequest.referrerPolicy = resource.referrerPolicy;" + - " fetchRequest.integrity = resource.integrity;" + - " fetchRequest.keepalive = resource.keepalive;" + - " } else {" + - " fetchRequest.url = resource;" + - " if (init != null) {" + - " fetchRequest.method = init.method;" + - " fetchRequest.headers = init.headers;" + - " fetchRequest.body = init.body;" + - " fetchRequest.mode = init.mode;" + - " fetchRequest.credentials = init.credentials;" + - " fetchRequest.cache = init.cache;" + - " fetchRequest.redirect = init.redirect;" + - " fetchRequest.referrer = init.referrer;" + - " fetchRequest.referrerPolicy = init.referrerPolicy;" + - " fetchRequest.integrity = init.integrity;" + - " fetchRequest.keepalive = init.keepalive;" + - " }" + - " }" + - " if (fetchRequest.headers instanceof Headers) {" + - " fetchRequest.headers = convertHeadersToJson(fetchRequest.headers);" + - " }" + - " fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials);" + - " return convertBodyToArray(fetchRequest.body).then(function(body) {" + - " fetchRequest.body = body;" + - " return window." + JavaScriptBridgeInterface.name + ".callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {" + - " if (result != null) {" + - " switch (result.action) {" + - " case 0:" + - " var controller = new AbortController();" + - " if (init != null) {" + - " init.signal = controller.signal;" + - " } else {" + - " init = {" + - " signal: controller.signal" + - " };" + - " }" + - " controller.abort();" + - " break;" + - " }" + - " resource = (result.url != null) ? result.url : resource;" + - " if (init == null) {" + - " init = {};" + - " }" + - " if (result.method != null && result.method.length > 0) {" + - " init.method = result.method;" + - " }" + - " if (result.headers != null && Object.keys(result.headers).length > 0) {" + - " init.headers = convertJsonToHeaders(result.headers);" + - " }" + - " if (result.body != null && result.body.length > 0) {" + - " init.body = convertArrayIntBodyToUint8Array(result.body);" + - " }" + - " if (result.mode != null && result.mode.length > 0) {" + - " init.mode = result.mode;" + - " }" + - " if (result.credentials != null) {" + - " init.credentials = convertJsonToCredential(result.credentials);" + - " }" + - " if (result.cache != null && result.cache.length > 0) {" + - " init.cache = result.cache;" + - " }" + - " if (result.redirect != null && result.redirect.length > 0) {" + - " init.redirect = result.redirect;" + - " }" + - " if (result.referrer != null && result.referrer.length > 0) {" + - " init.referrer = result.referrer;" + - " }" + - " if (result.referrerPolicy != null && result.referrerPolicy.length > 0) {" + - " init.referrerPolicy = result.referrerPolicy;" + - " }" + - " if (result.integrity != null && result.integrity.length > 0) {" + - " init.integrity = result.integrity;" + - " }" + - " if (result.keepalive != null) {" + - " init.keepalive = result.keepalive;" + - " }" + - " return fetch(resource, init);" + - " }" + - " return fetch(resource, init);" + - " });" + - " });" + - " } else {" + - " return fetch(resource, init);" + - " }" + - " };" + - "})(window.fetch);"; - - static final String isActiveElementInputEditableJS = - "var activeEl = document.activeElement;" + - "var nodeName = (activeEl != null) ? activeEl.nodeName.toLowerCase() : '';" + - "var isActiveElementInputEditable = activeEl != null && " + - "(activeEl.nodeType == 1 && (nodeName == 'textarea' || (nodeName == 'input' && /^(?:text|email|number|search|tel|url|password)$/i.test(activeEl.type != null ? activeEl.type : 'text')))) && " + - "!activeEl.disabled && !activeEl.readOnly;" + - "var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';"; - - static final String getSelectedTextJS = "(function(){" + - " var txt;" + - " if (window.getSelection) {" + - " txt = window.getSelection().toString();" + - " } else if (window.document.getSelection) {" + - " txt = window.document.getSelection().toString();" + - " } else if (window.document.selection) {" + - " txt = window.document.selection.createRange().text;" + - " }" + - " return txt;" + - "})();"; - - // android Workaround to hide context menu when selected text is empty - // and the document active element is not an input element. - static final String checkContextMenuShouldBeHiddenJS = "(function(){" + - " var txt;" + - " if (window.getSelection) {" + - " txt = window.getSelection().toString();" + - " } else if (window.document.getSelection) {" + - " txt = window.document.getSelection().toString();" + - " } else if (window.document.selection) {" + - " txt = window.document.selection.createRange().text;" + - " }" + - isActiveElementInputEditableJS + - " return txt === '' && !isActiveElementEditable;" + - "})();"; - - // android Workaround to hide context menu when user emit a keydown event - static final String checkGlobalKeyDownEventToHideContextMenuJS = "(function(){" + - " document.addEventListener('keydown', function(e) {" + - " window." + JavaScriptBridgeInterface.name + "._hideContextMenu();" + - " });" + - "})();"; - - static final String onWindowFocusEventJS = "(function(){" + - " window.addEventListener('focus', function(e) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onWindowFocus');" + - " });" + - "})();"; - - static final String onWindowBlurEventJS = "(function(){" + - " window.addEventListener('blur', function(e) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onWindowBlur');" + - " });" + - "})();"; - - static final String callAsyncJavaScriptWrapperJS = "(function(obj) {" + - " (async function($FUNCTION_ARGUMENT_NAMES) {" + - " $FUNCTION_BODY" + - " })($FUNCTION_ARGUMENT_VALUES).then(function(value) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('callAsyncJavaScript', {'value': value, 'error': null, 'resultUuid': '$RESULT_UUID'});" + - " }).catch(function(error) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('callAsyncJavaScript', {'value': null, 'error': error, 'resultUuid': '$RESULT_UUID'});" + - " });" + - " return null;" + - "})($FUNCTION_ARGUMENTS_OBJ);"; + public Map> callAsyncJavaScriptCallbacks = new HashMap<>(); + public Map> evaluateJavaScriptContentWorldCallbacks = new HashMap<>(); public InAppWebView(Context context) { super(context); @@ -694,21 +147,17 @@ final public class InAppWebView extends InputAwareWebView { super(context, attrs, defaultStyle); } - public InAppWebView(Context context, Object obj, Object id, - Integer windowId, InAppWebViewOptions options, - Map contextMenu, View containerView, - List> userScripts) { + public InAppWebView(Context context, MethodChannel channel, Object id, + @Nullable Integer windowId, InAppWebViewOptions options, + @Nullable Map contextMenu, View containerView, + List userScripts) { super(context, containerView); - if (obj instanceof InAppBrowserActivity) - this.inAppBrowserActivity = (InAppBrowserActivity) obj; - else if (obj instanceof FlutterWebView) - this.flutterWebView = (FlutterWebView) obj; - this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; + this.channel = channel; this.id = id; this.windowId = windowId; this.options = options; this.contextMenu = contextMenu; - this.userScripts = userScripts; + this.userContentController.addUserOnlyScripts(userScripts); Shared.activity.registerForContextMenu(this); } @@ -719,21 +168,19 @@ final public class InAppWebView extends InputAwareWebView { public void prepare() { - boolean isFromInAppBrowserActivity = inAppBrowserActivity != null; - httpClient = new OkHttpClient().newBuilder().build(); - javaScriptBridgeInterface = new JavaScriptBridgeInterface((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView); - addJavascriptInterface(javaScriptBridgeInterface, JavaScriptBridgeInterface.name); + javaScriptBridgeInterface = new JavaScriptBridgeInterface(this); + addJavascriptInterface(javaScriptBridgeInterface, JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); - inAppWebViewChromeClient = new InAppWebViewChromeClient((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView); + inAppWebViewChromeClient = new InAppWebViewChromeClient(channel, inAppBrowserDelegate); setWebChromeClient(inAppWebViewChromeClient); - inAppWebViewClient = new InAppWebViewClient((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView); + inAppWebViewClient = new InAppWebViewClient(channel, inAppBrowserDelegate); setWebViewClient(inAppWebViewClient); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { - inAppWebViewRenderProcessClient = new InAppWebViewRenderProcessClient((isFromInAppBrowserActivity) ? inAppBrowserActivity : flutterWebView); + inAppWebViewRenderProcessClient = new InAppWebViewRenderProcessClient(channel); WebViewCompat.setWebViewRenderProcessClient(this, inAppWebViewRenderProcessClient); } @@ -919,7 +366,7 @@ final public class InAppWebView extends InputAwareWebView { @Override public void run() { if (floatingContextMenu != null) { - evaluateJavascript(checkContextMenuShouldBeHiddenJS, new ValueCallback() { + evaluateJavascript(PluginScriptsUtil.CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE, new ValueCallback() { @Override public void onReceiveValue(String value) { if (value == null || value.equals("true")) { @@ -982,19 +429,33 @@ final public class InAppWebView extends InputAwareWebView { @Override public boolean onLongClick(View v) { HitTestResult hitTestResult = getHitTestResult(); - Map hitTestResultMap = new HashMap<>(); - hitTestResultMap.put("type", hitTestResult.getType()); - hitTestResultMap.put("extra", hitTestResult.getExtra()); - Map obj = new HashMap<>(); - obj.put("hitTestResult", hitTestResultMap); + obj.put("type", hitTestResult.getType()); + obj.put("extra", hitTestResult.getExtra()); channel.invokeMethod("onLongPressHitTestResult", obj); return false; } }); - } - private MotionEvent lastMotionEvent = null; + userContentController.addPluginScript(PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(ConsoleLogJS.CONSOLE_LOG_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(PrintJS.PRINT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT); + if (options.useShouldInterceptAjaxRequest) { + userContentController.addPluginScript(InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT); + } + if (options.useShouldInterceptFetchRequest) { + userContentController.addPluginScript(InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT); + } + if (options.useOnLoadResource) { + userContentController.addPluginScript(OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT); + } + if (!options.useHybridComposition) { + userContentController.addPluginScript(PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT); + } + } public void setIncognito(boolean enabled) { WebSettings settings = getSettings(); @@ -1039,73 +500,24 @@ final public class InAppWebView extends InputAwareWebView { } } - public void loadUrl(String url, MethodChannel.Result result) { - if (!url.isEmpty()) { - loadUrl(url); - } else { - result.error(LOG_TAG, "url is empty", null); - return; - } - result.success(true); - } - - public void loadUrl(String url, Map headers, MethodChannel.Result result) { - if (!url.isEmpty()) { - loadUrl(url, headers); - } else { - result.error(LOG_TAG, "url is empty", null); - return; - } - result.success(true); - } - - public void postUrl(String url, byte[] postData, MethodChannel.Result result) { - if (!url.isEmpty()) { + public void loadUrl(URLRequest urlRequest) { + String url = urlRequest.getUrl(); + String method = urlRequest.getMethod(); + if (method != null && method.equals("POST")) { + byte[] postData = urlRequest.getBody(); postUrl(url, postData); - } else { - result.error(LOG_TAG, "url is empty", null); return; } - result.success(true); - } - - public void loadData(String data, String mimeType, String encoding, String baseUrl, String historyUrl, MethodChannel.Result result) { - loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); - result.success(true); - } - - public void loadFile(String url, MethodChannel.Result result) { - try { - url = Util.getUrlAsset(url); - } catch (IOException e) { - result.error(LOG_TAG, url + " asset file cannot be found!", e); - return; - } - - if (!url.isEmpty()) { - loadUrl(url); - } else { - result.error(LOG_TAG, "url is empty", null); - return; - } - result.success(true); - } - - public void loadFile(String url, Map headers, MethodChannel.Result result) { - try { - url = Util.getUrlAsset(url); - } catch (IOException e) { - result.error(LOG_TAG, url + " asset file cannot be found!", e); - return; - } - - if (!url.isEmpty()) { + Map headers = urlRequest.getHeaders(); + if (headers != null) { loadUrl(url, headers); - } else { - result.error(LOG_TAG, "url is empty", null); return; } - result.success(true); + loadUrl(url); + } + + public void loadFile(String assetFilePath) throws IOException { + loadUrl(Util.getUrlAsset(assetFilePath)); } public boolean isLoading() { @@ -1228,37 +640,27 @@ final public class InAppWebView extends InputAwareWebView { settings.setJavaScriptEnabled(newOptions.javaScriptEnabled); if (newOptionsMap.get("useShouldInterceptAjaxRequest") != null && options.useShouldInterceptAjaxRequest != newOptions.useShouldInterceptAjaxRequest) { - String placeholderValue = newOptions.useShouldInterceptAjaxRequest ? "true" : "false"; - String sourceJs = InAppWebView.enableVariableForShouldInterceptAjaxRequestJS.replace("$PLACEHOLDER_VALUE", placeholderValue); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - for (String contentWorldName : userScriptsContentWorlds) { - evaluateJavascript(sourceJs, contentWorldName, null); - } - } else { - loadUrl("javascript:" + sourceJs); - } + enablePluginScriptAtRuntime( + InterceptAjaxRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + newOptions.useShouldInterceptAjaxRequest, + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT + ); } if (newOptionsMap.get("useShouldInterceptFetchRequest") != null && options.useShouldInterceptFetchRequest != newOptions.useShouldInterceptFetchRequest) { - String placeholderValue = newOptions.useShouldInterceptFetchRequest ? "true" : "false"; - String sourceJs = InAppWebView.enableVariableForShouldInterceptFetchRequestsJS.replace("$PLACEHOLDER_VALUE", placeholderValue); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - for (String contentWorldName : userScriptsContentWorlds) { - evaluateJavascript(sourceJs, contentWorldName, null); - } - } else { - loadUrl("javascript:" + sourceJs); - } + enablePluginScriptAtRuntime( + InterceptFetchRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + newOptions.useShouldInterceptFetchRequest, + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT + ); } if (newOptionsMap.get("useOnLoadResource") != null && options.useOnLoadResource != newOptions.useOnLoadResource) { - String placeholderValue = newOptions.useOnLoadResource ? "true" : "false"; - String sourceJs = InAppWebView.enableVariableForOnLoadResourceJS.replace("$PLACEHOLDER_VALUE", placeholderValue); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - evaluateJavascript(sourceJs, (ValueCallback) null); - } else { - loadUrl("javascript:" + sourceJs); - } + enablePluginScriptAtRuntime( + OnLoadResourceJS.FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + newOptions.useOnLoadResource, + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT + ); } if (newOptionsMap.get("javaScriptCanOpenWindowsAutomatically") != null && options.javaScriptCanOpenWindowsAutomatically != newOptions.javaScriptCanOpenWindowsAutomatically) @@ -1523,7 +925,29 @@ final public class InAppWebView extends InputAwareWebView { return (options != null) ? options.getRealOptions(this) : null; } - public void injectDeferredObject(String source, @Nullable final String contentWorldName, String jsWrapper, @Nullable final MethodChannel.Result result) { + public void enablePluginScriptAtRuntime(final String flagVariable, + final boolean enable, + final PluginScript pluginScript) { + evaluateJavascript("window." + flagVariable, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + boolean alreadyLoaded = value != null && !value.equalsIgnoreCase("null"); + if (alreadyLoaded) { + String enableSource = "window." + flagVariable + " = " + enable + ";"; + evaluateJavascript(enableSource, null, null); + if (!enable) { + userContentController.removePluginScript(pluginScript); + } + } else if (enable) { + evaluateJavascript(pluginScript.getSource(), null, null); + userContentController.addPluginScript(pluginScript); + } + } + }); + } + + public void injectDeferredObject(String source, @Nullable final ContentWorld contentWorld, String jsWrapper, @Nullable final ValueCallback resultCallback) { + final String resultUuid = contentWorld != null ? UUID.randomUUID().toString() : null; String scriptToInject = source; if (jsWrapper != null) { org.json.JSONArray jsonEsc = new org.json.JSONArray(); @@ -1532,49 +956,39 @@ final public class InAppWebView extends InputAwareWebView { String jsonSourceString = jsonRepr.substring(1, jsonRepr.length() - 1); scriptToInject = String.format(jsWrapper, jsonSourceString); } + if (resultUuid != null && resultCallback != null) { + evaluateJavaScriptContentWorldCallbacks.put(resultUuid, resultCallback); + scriptToInject = PluginScriptsUtil.EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, UserContentController.escapeCode(source)) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + } final String finalScriptToInject = scriptToInject; headlessHandler.post(new Runnable() { @Override public void run() { + String scriptToInject = userContentController.generateCodeForScriptEvaluation(finalScriptToInject, contentWorld); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // This action will have the side-effect of blurring the currently focused element - loadUrl("javascript:" + finalScriptToInject.replaceAll("[\r\n]+", "")); - result.success(""); - } else { - if (contentWorldName != null && !contentWorldName.equals("page")) { - String sourceToInject = finalScriptToInject; - if (!userScriptsContentWorlds.contains(contentWorldName)) { - userScriptsContentWorlds.add(contentWorldName); - // Add only the first time all the plugin scripts needed. - String jsPluginScripts = prepareAndWrapPluginUserScripts(); - sourceToInject = jsPluginScripts + "\n" + sourceToInject; - } - sourceToInject = wrapSourceCodeInContentWorld(contentWorldName, sourceToInject); - evaluateJavascript(sourceToInject, new ValueCallback() { - @Override - public void onReceiveValue(String s) { - if (result == null) - return; - result.success(s); - } - }); - } else { - evaluateJavascript(finalScriptToInject, new ValueCallback() { - @Override - public void onReceiveValue(String s) { - if (result == null) - return; - result.success(s); - } - }); + loadUrl("javascript:" + scriptToInject.replaceAll("[\r\n]+", "")); + if (contentWorld != null && resultCallback != null) { + resultCallback.onReceiveValue(""); } + } else { + evaluateJavascript(scriptToInject, new ValueCallback() { + @Override + public void onReceiveValue(String s) { + if (contentWorld != null || resultCallback == null) + return; + resultCallback.onReceiveValue(s); + } + }); } } }); } - public void evaluateJavascript(String source, @Nullable String contentWorldName, MethodChannel.Result result) { - injectDeferredObject(source, contentWorldName, null, result); + public void evaluateJavascript(String source, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { + injectDeferredObject(source, contentWorld, null, resultCallback); } public void injectJavascriptFileFromUrl(String urlFile, @Nullable Map scriptHtmlTagAttributes) { @@ -2070,7 +1484,7 @@ final public class InAppWebView extends InputAwareWebView { @RequiresApi(api = Build.VERSION_CODES.KITKAT) public void getSelectedText(final ValueCallback resultCallback) { - evaluateJavascript(getSelectedTextJS, new ValueCallback() { + evaluateJavascript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, new ValueCallback() { @Override public void onReceiveValue(String value) { value = (value != null && !value.equalsIgnoreCase("null")) ? value.substring(1, value.length() - 1) : null; @@ -2113,124 +1527,12 @@ final public class InAppWebView extends InputAwareWebView { return obj; } - public Map getCertificateMap() { - return InAppWebView.getCertificateMap(getCertificate()); - } - - public static Map getCertificateMap(SslCertificate sslCertificate) { - if (sslCertificate != null) { - SslCertificate.DName issuedByName = sslCertificate.getIssuedBy(); - Map issuedBy = new HashMap<>(); - issuedBy.put("CName", issuedByName.getCName()); - issuedBy.put("DName", issuedByName.getDName()); - issuedBy.put("OName", issuedByName.getOName()); - issuedBy.put("UName", issuedByName.getUName()); - - SslCertificate.DName issuedToName = sslCertificate.getIssuedTo(); - Map issuedTo = new HashMap<>(); - issuedTo.put("CName", issuedToName.getCName()); - issuedTo.put("DName", issuedToName.getDName()); - issuedTo.put("OName", issuedToName.getOName()); - issuedTo.put("UName", issuedToName.getUName()); - - byte[] x509CertificateData = null; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - X509Certificate certificate = sslCertificate.getX509Certificate(); - if (certificate != null) { - x509CertificateData = certificate.getEncoded(); - } - } catch (CertificateEncodingException e) { - e.printStackTrace(); - } - } else { - try { - x509CertificateData = Util.getX509CertFromSslCertHack(sslCertificate).getEncoded(); - } catch (CertificateEncodingException e) { - e.printStackTrace(); - } - } - - Map obj = new HashMap<>(); - obj.put("issuedBy", issuedBy); - obj.put("issuedTo", issuedTo); - obj.put("validNotAfterDate", sslCertificate.getValidNotAfterDate().getTime()); - obj.put("validNotBeforeDate", sslCertificate.getValidNotBeforeDate().getTime()); - obj.put("x509Certificate", x509CertificateData); - - return obj; - } - - return null; - } - - public boolean addUserScript(Map userScript) { - String contentWorldName = (String) userScript.get("contentWorld"); - if (contentWorldName != null && !userScriptsContentWorlds.contains(contentWorldName)) { - userScriptsContentWorlds.add(contentWorldName); - } - return userScripts.add(userScript); - } - - public Map removeUserScript(int index) { - return userScripts.remove(index); - } - - public void removeAllUserScripts() { - userScripts.clear(); - } - - public void resetUserScriptsContentWorlds() { - userScriptsContentWorlds.clear(); - userScriptsContentWorlds.add("page"); - } - - public String prepareAndWrapPluginUserScripts() { - String js = JavaScriptBridgeInterface.callHandlerScriptJS; - js += InAppWebView.consoleLogJS; - if (options.useShouldInterceptAjaxRequest) { - js += InAppWebView.interceptAjaxRequestsJS; - } - if (options.useShouldInterceptFetchRequest) { - js += InAppWebView.interceptFetchRequestsJS; - } - if (options.useOnLoadResource) { - js += InAppWebView.resourceObserverJS; - } - if (!options.useHybridComposition) { - js += InAppWebView.checkGlobalKeyDownEventToHideContextMenuJS; - } - js += InAppWebView.onWindowFocusEventJS; - js += InAppWebView.onWindowBlurEventJS; - js += InAppWebView.printJS; - - String jsWrapped = InAppWebView.pluginScriptsWrapperJS - .replace("$PLACEHOLDER_VALUE", js); - - return jsWrapped; - } - - public String wrapSourceCodeInContentWorld(@Nullable String contentWorldName, String source) { - JSONObject sourceEncoded = new JSONObject(); - try { - // encode the javascript source in order to escape special chars and quotes - sourceEncoded.put("source", source); - } catch (JSONException e) { - e.printStackTrace(); - } - - String sourceWrapped = contentWorldName == null || contentWorldName.equals("page") ? source : - InAppWebView.contentWorldWrapperJS.replace("$CONTENT_WORLD_NAME", contentWorldName) - .replace("$JSON_SOURCE_ENCODED", sourceEncoded.toString()); - - return sourceWrapped; - } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public void callAsyncJavaScript(String functionBody, Map arguments, @Nullable String contentWorldName, @NonNull MethodChannel.Result result) { + public void callAsyncJavaScript(String functionBody, Map arguments, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { String resultUuid = UUID.randomUUID().toString(); - callAsyncJavaScriptResults.put(resultUuid, result); + if (resultCallback != null) { + callAsyncJavaScriptCallbacks.put(resultUuid, resultCallback); + } JSONObject functionArguments = new JSONObject(arguments); Iterator keys = functionArguments.keys(); @@ -2247,41 +1549,52 @@ final public class InAppWebView extends InputAwareWebView { String functionArgumentValues = TextUtils.join(", ", functionArgumentValuesList); String functionArgumentsObj = Util.JSONStringify(arguments); - String sourceToInject = InAppWebView.callAsyncJavaScriptWrapperJS - .replace("$FUNCTION_ARGUMENT_NAMES", functionArgumentNames) - .replace("$FUNCTION_ARGUMENT_VALUES", functionArgumentValues) - .replace("$FUNCTION_ARGUMENTS_OBJ", functionArgumentsObj) - .replace("$FUNCTION_BODY", functionBody) - .replace("$RESULT_UUID", resultUuid); - - if (contentWorldName != null && !contentWorldName.equals("page")) { - if (!userScriptsContentWorlds.contains(contentWorldName)) { - userScriptsContentWorlds.add(contentWorldName); - // Add only the first time all the plugin scripts needed. - String jsPluginScripts = prepareAndWrapPluginUserScripts(); - sourceToInject = jsPluginScripts + "\n" + sourceToInject; - } - sourceToInject = wrapSourceCodeInContentWorld(contentWorldName, sourceToInject); - - } + String sourceToInject = PluginScriptsUtil.CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, functionArgumentNames) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, functionArgumentValues) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, functionArgumentsObj) + .replace(PluginScriptsUtil.VAR_FUNCTION_BODY, functionBody) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + sourceToInject = userContentController.generateCodeForScriptEvaluation(sourceToInject, contentWorld); evaluateJavascript(sourceToInject, null); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void isSecureContext(final ValueCallback resultCallback) { + evaluateJavascript("window.isSecureContext", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("null") + || value.equalsIgnoreCase("false")) { + resultCallback.onReceiveValue(false); + return; + } + resultCallback.onReceiveValue(true); + } + }); + } + @Override public void dispose() { - if (windowId != null && InAppWebViewChromeClient.windowWebViewMessages.containsKey(windowId)) { + if (windowId != null) { InAppWebViewChromeClient.windowWebViewMessages.remove(windowId); } headlessHandler.removeCallbacksAndMessages(null); mHandler.removeCallbacksAndMessages(null); - removeJavascriptInterface(JavaScriptBridgeInterface.name); removeAllViews(); if (checkContextMenuShouldBeClosedTask != null) removeCallbacks(checkContextMenuShouldBeClosedTask); if (checkScrollStoppedTask != null) removeCallbacks(checkScrollStoppedTask); - callAsyncJavaScriptResults.clear(); + callAsyncJavaScriptCallbacks.clear(); + evaluateJavaScriptContentWorldCallbacks.clear(); + inAppBrowserDelegate = null; + inAppWebViewChromeClient = null; + inAppWebViewClient = null; + javaScriptBridgeInterface = null; + inAppWebViewRenderProcessClient = null; super.dispose(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java similarity index 91% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java index b7078ad0..8393f69c 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewChromeClient.java @@ -1,9 +1,8 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; -import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; @@ -41,14 +40,16 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview.types.CreateWindowAction; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.ActivityResultListener; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserDelegate; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -61,12 +62,11 @@ import io.flutter.plugin.common.PluginRegistry; import static android.app.Activity.RESULT_OK; -public class InAppWebViewChromeClient extends WebChromeClient implements PluginRegistry.ActivityResultListener, InAppBrowserActivity.ActivityResultListener { +public class InAppWebViewChromeClient extends WebChromeClient implements PluginRegistry.ActivityResultListener, ActivityResultListener { protected static final String LOG_TAG = "IABWebChromeClient"; - private FlutterWebView flutterWebView; - private InAppBrowserActivity inAppBrowserActivity; - public MethodChannel channel; + private InAppBrowserDelegate inAppBrowserDelegate; + private final MethodChannel channel; public static Map windowWebViewMessages = new HashMap<>(); private static int windowAutoincrementId = 0; @@ -101,15 +101,14 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR private int mOriginalOrientation; private int mOriginalSystemUiVisibility; - public InAppWebViewChromeClient(Object obj) { - if (obj instanceof InAppBrowserActivity) { - this.inAppBrowserActivity = (InAppBrowserActivity) obj; - this.inAppBrowserActivity.activityResultListeners.add(this); + public InAppWebViewChromeClient(MethodChannel channel, InAppBrowserDelegate inAppBrowserDelegate) { + super(); + + this.channel = channel; + this.inAppBrowserDelegate = inAppBrowserDelegate; + if (this.inAppBrowserDelegate != null) { + this.inAppBrowserDelegate.getActivityResultListeners().add(this); } - else if (obj instanceof FlutterWebView) { - this.flutterWebView = (FlutterWebView) obj; - } - this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; if (Shared.registrar != null) Shared.registrar.addActivityResultListener(this); @@ -122,13 +121,13 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR if (mCustomView == null) { return null; } - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; return BitmapFactory.decodeResource(activity.getApplicationContext().getResources(), 2130837573); } @Override public void onHideCustomView() { - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; View decorView = getRootView(); ((FrameLayout) decorView).removeView(this.mCustomView); @@ -149,7 +148,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR return; } - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; View decorView = getRootView(); this.mCustomView = paramView; @@ -233,7 +232,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } }; - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Dialog_Alert); alertDialogBuilder.setMessage(alertMessage); @@ -326,7 +325,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } }; - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Dialog_Alert); alertDialogBuilder.setMessage(alertMessage); @@ -445,7 +444,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } }; - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Dialog_Alert); alertDialogBuilder.setMessage(alertMessage); @@ -479,7 +478,6 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR Map obj = new HashMap<>(); obj.put("url", url); obj.put("message", message); - obj.put("iosIsMainFrame", null); channel.invokeMethod("onJsBeforeUnload", obj, new MethodChannel.Result() { @Override @@ -544,7 +542,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } }; - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, R.style.Theme_AppCompat_Dialog_Alert); alertDialogBuilder.setMessage(alertMessage); @@ -579,25 +577,19 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR WebView.HitTestResult result = view.getHitTestResult(); String url = result.getExtra(); - final Map obj = new HashMap<>(); - obj.put("url", url); - obj.put("windowId", windowId); - obj.put("androidIsDialog", isDialog); - obj.put("androidIsUserGesture", isUserGesture); - obj.put("iosWKNavigationType", null); - obj.put("iosIsForMainFrame", null); - obj.put("iosAllowsCellularAccess", null); - obj.put("iosAllowsConstrainedNetworkAccess", null); - obj.put("iosAllowsExpensiveNetworkAccess", null); - obj.put("iosCachePolicy", null); - obj.put("iosHttpShouldHandleCookies", null); - obj.put("iosHttpShouldUsePipelining", null); - obj.put("iosNetworkServiceType", null); - obj.put("iosTimeoutInterval", null); + URLRequest request = new URLRequest(url, "GET", null, null); + CreateWindowAction createWindowAction = new CreateWindowAction( + request, + true, + isUserGesture, + false, + windowId, + isDialog + ); windowWebViewMessages.put(windowId, resultMsg); - channel.invokeMethod("onCreateWindow", obj, new MethodChannel.Result() { + channel.invokeMethod("onCreateWindow", createWindowAction.toMap(), new MethodChannel.Result() { @Override public void success(@Nullable Object result) { boolean handledByClient = false; @@ -678,30 +670,23 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR @Override public void onProgressChanged(WebView view, int progress) { - if (inAppBrowserActivity != null && inAppBrowserActivity.progressBar != null) { - inAppBrowserActivity.progressBar.setVisibility(View.VISIBLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - inAppBrowserActivity.progressBar.setProgress(progress, true); - } else { - inAppBrowserActivity.progressBar.setProgress(progress); - } - if (progress == 100) { - inAppBrowserActivity.progressBar.setVisibility(View.GONE); - } + super.onProgressChanged(view, progress); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeProgress(progress); } Map obj = new HashMap<>(); obj.put("progress", progress); channel.invokeMethod("onProgressChanged", obj); - - super.onProgressChanged(view, progress); } @Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); - if (inAppBrowserActivity != null && inAppBrowserActivity.actionBar != null && inAppBrowserActivity.options.toolbarTopFixedTitle.isEmpty()) { - inAppBrowserActivity.actionBar.setTitle(title); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeTitle(title); } Map obj = new HashMap<>(); @@ -744,7 +729,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } protected ViewGroup getRootView() { - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; return (ViewGroup) activity.findViewById(android.R.id.content); } @@ -838,7 +823,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } private boolean isFileNotEmpty(Uri uri) { - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; long length; try { @@ -879,7 +864,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) { activity.startActivityForResult(chooserIntent, PICKER_LEGACY); } else { @@ -907,7 +892,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) { activity.startActivityForResult(chooserIntent, PICKER); } else { @@ -920,7 +905,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR protected boolean needsCameraPermission() { boolean needed = false; - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; PackageManager packageManager = activity.getPackageManager(); try { String[] requestedPermissions = packageManager.getPackageInfo(activity.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; @@ -1062,7 +1047,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR return Uri.fromFile(capturedFile); } - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions String packageName = activity.getApplicationContext().getPackageName(); return FileProvider.getUriForFile(activity.getApplicationContext(), packageName + "." + fileProviderAuthorityExtension, capturedFile); @@ -1092,7 +1077,7 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR return new File(storageDir, filename); } - Activity activity = inAppBrowserActivity != null ? inAppBrowserActivity : Shared.activity; + Activity activity = inAppBrowserDelegate != null ? inAppBrowserDelegate.getActivity() : Shared.activity; File storageDir = activity.getApplicationContext().getExternalFilesDir(null); return File.createTempFile(prefix, suffix, storageDir); } @@ -1150,15 +1135,12 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR } public void dispose() { - channel.setMethodCallHandler(null); if (Shared.activityPluginBinding != null) { Shared.activityPluginBinding.removeActivityResultListener(this); } - if (inAppBrowserActivity != null) { - inAppBrowserActivity = null; - } - if (flutterWebView != null) { - flutterWebView = null; + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.getActivityResultListeners().clear(); + inAppBrowserDelegate = null; } } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java similarity index 69% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java index 591a0638..455d9358 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.annotation.TargetApi; import android.graphics.Bitmap; @@ -23,10 +23,18 @@ import android.webkit.WebViewClient; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import com.pichillilorenzo.flutter_inappwebview.CredentialDatabase.Credential; -import com.pichillilorenzo.flutter_inappwebview.CredentialDatabase.CredentialDatabase; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; import com.pichillilorenzo.flutter_inappwebview.Util; +import com.pichillilorenzo.flutter_inappwebview.credential_database.CredentialDatabase; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview.types.URLProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; import java.io.ByteArrayInputStream; import java.net.URI; @@ -42,19 +50,16 @@ import io.flutter.plugin.common.MethodChannel; public class InAppWebViewClient extends WebViewClient { protected static final String LOG_TAG = "IAWebViewClient"; - private FlutterWebView flutterWebView; - private InAppBrowserActivity inAppBrowserActivity; - public MethodChannel channel; + private InAppBrowserDelegate inAppBrowserDelegate; + private final MethodChannel channel; private static int previousAuthRequestFailureCount = 0; - private static List credentialsProposed = null; + private static List credentialsProposed = null; - public InAppWebViewClient(Object obj) { + public InAppWebViewClient(MethodChannel channel, InAppBrowserDelegate inAppBrowserDelegate) { super(); - if (obj instanceof InAppBrowserActivity) - this.inAppBrowserActivity = (InAppBrowserActivity) obj; - else if (obj instanceof FlutterWebView) - this.flutterWebView = (FlutterWebView) obj; - this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; + + this.channel = channel; + this.inAppBrowserDelegate = inAppBrowserDelegate; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -62,34 +67,24 @@ public class InAppWebViewClient extends WebViewClient { public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { InAppWebView webView = (InAppWebView) view; if (webView.options.useShouldOverrideUrlLoading) { + boolean isRedirect = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - onShouldOverrideUrlLoading( - webView, - request.getUrl().toString(), - request.getMethod(), - request.getRequestHeaders(), - request.isForMainFrame(), - request.hasGesture(), - request.isRedirect()); - } else { - onShouldOverrideUrlLoading( - webView, - request.getUrl().toString(), - request.getMethod(), - request.getRequestHeaders(), - request.isForMainFrame(), - request.hasGesture(), - false); + isRedirect = request.isRedirect(); } + onShouldOverrideUrlLoading( + webView, + request.getUrl().toString(), + request.getMethod(), + request.getRequestHeaders(), + request.isForMainFrame(), + request.hasGesture(), + isRedirect); if (webView.regexToCancelSubFramesLoadingCompiled != null) { if (request.isForMainFrame()) return true; else { Matcher m = webView.regexToCancelSubFramesLoadingCompiled.matcher(request.getUrl().toString()); - if (m.matches()) - return true; - else - return false; + return m.matches(); } } else { // There isn't any way to load an URL for a frame that is not the main frame, @@ -110,154 +105,99 @@ public class InAppWebViewClient extends WebViewClient { return false; } + private void allowShouldOverrideUrlLoading(WebView webView, String url, Map headers, boolean isForMainFrame) { + if (isForMainFrame) { + // There isn't any way to load an URL for a frame that is not the main frame, + // so call this only on main frame. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + webView.loadUrl(url, headers); + else + webView.loadUrl(url); + } + } public void onShouldOverrideUrlLoading(final InAppWebView webView, final String url, final String method, final Map headers, final boolean isForMainFrame, boolean hasGesture, boolean isRedirect) { - Map obj = new HashMap<>(); - obj.put("url", url); - obj.put("method", method); - obj.put("headers", headers); - obj.put("isForMainFrame", isForMainFrame); - obj.put("androidHasGesture", hasGesture); - obj.put("androidIsRedirect", isRedirect); - obj.put("iosWKNavigationType", null); - obj.put("iosAllowsCellularAccess", null); - obj.put("iosAllowsConstrainedNetworkAccess", null); - obj.put("iosAllowsExpensiveNetworkAccess", null); - obj.put("iosCachePolicy", null); - obj.put("iosHttpShouldHandleCookies", null); - obj.put("iosHttpShouldUsePipelining", null); - obj.put("iosNetworkServiceType", null); - obj.put("iosTimeoutInterval", null); + URLRequest request = new URLRequest(url, method, null, headers); + NavigationAction navigationAction = new NavigationAction( + request, + isForMainFrame, + hasGesture, + isRedirect + ); - channel.invokeMethod("shouldOverrideUrlLoading", obj, new MethodChannel.Result() { + channel.invokeMethod("shouldOverrideUrlLoading", navigationAction.toMap(), new MethodChannel.Result() { @Override public void success(Object response) { - if (response != null) { + if (response != null) { Map responseMap = (Map) response; Integer action = (Integer) responseMap.get("action"); - if (action != null) { - switch (action) { - case 1: - if (isForMainFrame) { - // There isn't any way to load an URL for a frame that is not the main frame, - // so call this only on main frame. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - webView.loadUrl(url, headers); - else - webView.loadUrl(url); - } + action = action != null ? action : NavigationActionPolicy.CANCEL.rawValue(); + + NavigationActionPolicy navigationActionPolicy = NavigationActionPolicy.fromValue(action); + if (navigationActionPolicy != null) { + switch (navigationActionPolicy) { + case ALLOW: + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); return; - case 0: + case CANCEL: default: return; } } + return; } + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); } @Override public void error(String s, String s1, Object o) { - Log.d(LOG_TAG, "ERROR: " + s + " " + s1); + Log.e(LOG_TAG, "ERROR: " + s + " " + s1); + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); } @Override public void notImplemented() { - + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); } }); } - private void loadCustomJavaScriptOnPageStarted(WebView view) { + public void loadCustomJavaScriptOnPageStarted(WebView view) { InAppWebView webView = (InAppWebView) view; - String jsPluginScriptsWrapped = webView.prepareAndWrapPluginUserScripts(); - String jsUserScriptsAtDocumentStart = prepareUserScriptsAtDocumentStart(webView); - - String js = wrapPluginAndUserScripts(jsPluginScriptsWrapped, jsUserScriptsAtDocumentStart, null); + String source = webView.userContentController.generateWrappedCodeForDocumentStart(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.evaluateJavascript(js, (ValueCallback) null); + webView.evaluateJavascript(source, (ValueCallback) null); } else { - webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); } } - private void loadCustomJavaScriptOnPageFinished(WebView view) { + public void loadCustomJavaScriptOnPageFinished(WebView view) { InAppWebView webView = (InAppWebView) view; - // try to reload also custom scripts if they were not loaded during the onPageStarted event - String jsPluginScriptsWrapped = webView.prepareAndWrapPluginUserScripts(); - String jsUserScriptsAtDocumentStart = prepareUserScriptsAtDocumentStart(webView); - String jsUserScriptsAtDocumentEnd = prepareUserScriptsAtDocumentEnd(webView); - - String js = wrapPluginAndUserScripts(jsPluginScriptsWrapped, jsUserScriptsAtDocumentStart, jsUserScriptsAtDocumentEnd); + String source = webView.userContentController.generateWrappedCodeForDocumentEnd(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.evaluateJavascript(js, (ValueCallback) null); + webView.evaluateJavascript(source, (ValueCallback) null); } else { - webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); } } - - private String prepareUserScripts(InAppWebView webView, int atDocumentInjectionTime) { - StringBuilder js = new StringBuilder(); - - for (Map userScript : webView.userScripts) { - Integer injectionTime = (Integer) userScript.get("injectionTime"); - if ((injectionTime == null && atDocumentInjectionTime == 0) || (injectionTime != null && injectionTime == atDocumentInjectionTime)) { - String source = (String) userScript.get("source"); - String contentWorldName = (String) userScript.get("contentWorld"); - if (source != null) { - if (contentWorldName != null && !contentWorldName.equals("page")) { - String jsPluginScripts = webView.prepareAndWrapPluginUserScripts(); - source = jsPluginScripts + "\n" + source; - } - if (contentWorldName != null && !webView.userScriptsContentWorlds.contains(contentWorldName)) { - webView.userScriptsContentWorlds.add(contentWorldName); - } - String sourceWrapped = webView.wrapSourceCodeInContentWorld(contentWorldName, source); - if (atDocumentInjectionTime == 0 && contentWorldName != null && !contentWorldName.equals("page")) { - // adds another wrapper because sometimes document.body is not ready and it is undefined, causing an error and not adding the iframe element. - sourceWrapped = InAppWebView.documentReadyWrapperJS.replace("$PLACEHOLDER_VALUE", sourceWrapped) - .replace("$PLACEHOLDER_VALUE", sourceWrapped); - } - - js.append(sourceWrapped); - } - } - } - - return js.toString(); - } - - private String prepareUserScriptsAtDocumentStart(InAppWebView webView) { - return prepareUserScripts(webView, 0); - } - - private String prepareUserScriptsAtDocumentEnd(InAppWebView webView) { - return prepareUserScripts(webView, 1); - } - - private String wrapPluginAndUserScripts(String jsPluginScriptsWrapped, @Nullable String jsUserScriptsAtDocumentStart, @Nullable String jsUserScriptsAtDocumentEnd) { - String jsUserScriptsAtDocumentStartWrapped = jsUserScriptsAtDocumentStart == null || jsUserScriptsAtDocumentStart.isEmpty() ? "" : - InAppWebView.userScriptsAtDocumentStartWrapperJS.replace("$PLACEHOLDER_VALUE", jsUserScriptsAtDocumentStart); - String jsUserScriptsAtDocumentEndWrapped = jsUserScriptsAtDocumentEnd == null || jsUserScriptsAtDocumentEnd.isEmpty() ? "" : - InAppWebView.userScriptsAtDocumentEndWrapperJS.replace("$PLACEHOLDER_VALUE", jsUserScriptsAtDocumentEnd); - return jsPluginScriptsWrapped + "\n" + jsUserScriptsAtDocumentStartWrapped + "\n" + jsUserScriptsAtDocumentEndWrapped; - } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { final InAppWebView webView = (InAppWebView) view; - webView.resetUserScriptsContentWorlds(); + webView.isLoading = true; + webView.userContentController.resetContentWorlds(); loadCustomJavaScriptOnPageStarted(webView); super.onPageStarted(view, url, favicon); - webView.isLoading = true; - if (inAppBrowserActivity != null && inAppBrowserActivity.searchView != null && !url.equals(inAppBrowserActivity.searchView.getQuery().toString())) { - inAppBrowserActivity.searchView.setQuery(url, false); + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didStartNavigation(url); } Map obj = new HashMap<>(); @@ -268,14 +208,16 @@ public class InAppWebViewClient extends WebViewClient { public void onPageFinished(WebView view, String url) { final InAppWebView webView = (InAppWebView) view; - + webView.isLoading = false; loadCustomJavaScriptOnPageFinished(webView); + previousAuthRequestFailureCount = 0; + credentialsProposed = null; super.onPageFinished(view, url); - webView.isLoading = false; - previousAuthRequestFailureCount = 0; - credentialsProposed = null; + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFinishNavigation(url); + } // WebView not storing cookies reliable to local device storage if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -284,7 +226,7 @@ public class InAppWebViewClient extends WebViewClient { CookieSyncManager.getInstance().sync(); } - String js = InAppWebView.platformReadyJS; + String js = JavaScriptBridgeJS.PLATFORM_READY_JS_SOURCE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(js, (ValueCallback) null); @@ -299,17 +241,23 @@ public class InAppWebViewClient extends WebViewClient { @Override public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + super.doUpdateVisitedHistory(view, url, isReload); + + url = view.getUrl(); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didUpdateVisitedHistory(url); + } + Map obj = new HashMap<>(); + // url argument sometimes doesn't contain the new changed URL, so we get it again from the webview. obj.put("url", url); obj.put("androidIsReload", isReload); channel.invokeMethod("onUpdateVisitedHistory", obj); - - super.doUpdateVisitedHistory(view, url, isReload); } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - final InAppWebView webView = (InAppWebView) view; if (webView.options.disableDefaultErrorPage) { @@ -321,6 +269,10 @@ public class InAppWebViewClient extends WebViewClient { previousAuthRequestFailureCount = 0; credentialsProposed = null; + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(failingUrl, errorCode, description); + } + Map obj = new HashMap<>(); obj.put("url", failingUrl); obj.put("code", errorCode); @@ -343,9 +295,6 @@ public class InAppWebViewClient extends WebViewClient { } } - /** - * On received http auth request. - */ @Override public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { @@ -374,7 +323,18 @@ public class InAppWebViewClient extends WebViewClient { obj.put("port", port); obj.put("previousFailureCount", previousAuthRequestFailureCount); - channel.invokeMethod("onReceivedHttpAuthRequest", obj, new MethodChannel.Result() { + if (credentialsProposed == null) + credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); + + URLCredential credentialProposed = null; + if (credentialsProposed != null && credentialsProposed.size() > 0) { + credentialProposed = credentialsProposed.get(0); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + HttpAuthenticationChallenge challenge = new HttpAuthenticationChallenge(protectionSpace, previousAuthRequestFailureCount, credentialProposed); + + channel.invokeMethod("onReceivedHttpAuthRequest", challenge.toMap(), new MethodChannel.Result() { @Override public void success(Object response) { if (response != null) { @@ -386,17 +346,15 @@ public class InAppWebViewClient extends WebViewClient { String username = (String) responseMap.get("username"); String password = (String) responseMap.get("password"); Boolean permanentPersistence = (Boolean) responseMap.get("permanentPersistence"); - if (permanentPersistence != null && permanentPersistence && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (permanentPersistence != null && permanentPersistence) { CredentialDatabase.getInstance(view.getContext()).setHttpAuthCredential(host, protocol, realm, port, username, password); } handler.proceed(username, password); return; case 2: - if (credentialsProposed == null) - credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); if (credentialsProposed.size() > 0) { - Credential credential = credentialsProposed.remove(0); - handler.proceed(credential.username, credential.password); + URLCredential credential = credentialsProposed.remove(0); + handler.proceed(credential.getUsername(), credential.getPassword()); } else { handler.cancel(); } @@ -429,10 +387,10 @@ public class InAppWebViewClient extends WebViewClient { } @Override - public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError sslError) { URI uri; try { - uri = new URI(view.getUrl()); + uri = new URI(sslError.getUrl()); } catch (URISyntaxException e) { e.printStackTrace(); handler.cancel(); @@ -444,40 +402,10 @@ public class InAppWebViewClient extends WebViewClient { final String realm = null; final int port = uri.getPort(); - Map obj = new HashMap<>(); - obj.put("host", host); - obj.put("protocol", protocol); - obj.put("realm", realm); - obj.put("port", port); - obj.put("androidError", error.getPrimaryError()); - obj.put("iosError", null); - obj.put("sslCertificate", InAppWebView.getCertificateMap(error.getCertificate())); + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, sslError.getCertificate(), sslError); + ServerTrustChallenge challenge = new ServerTrustChallenge(protectionSpace); - String message; - switch (error.getPrimaryError()) { - case SslError.SSL_DATE_INVALID: - message = "The date of the certificate is invalid"; - break; - case SslError.SSL_EXPIRED: - message = "The certificate has expired"; - break; - case SslError.SSL_IDMISMATCH: - message = "Hostname mismatch"; - break; - default: - case SslError.SSL_INVALID: - message = "A generic error occurred"; - break; - case SslError.SSL_NOTYETVALID: - message = "The certificate is not yet valid"; - break; - case SslError.SSL_UNTRUSTED: - message = "The certificate authority is not trusted"; - break; - } - obj.put("message", message); - - channel.invokeMethod("onReceivedServerTrustAuthRequest", obj, new MethodChannel.Result() { + channel.invokeMethod("onReceivedServerTrustAuthRequest", challenge.toMap(), new MethodChannel.Result() { @Override public void success(Object response) { if (response != null) { @@ -496,7 +424,7 @@ public class InAppWebViewClient extends WebViewClient { } } - InAppWebViewClient.super.onReceivedSslError(view, handler, error); + InAppWebViewClient.super.onReceivedSslError(view, handler, sslError); } @Override @@ -506,7 +434,7 @@ public class InAppWebViewClient extends WebViewClient { @Override public void notImplemented() { - InAppWebViewClient.super.onReceivedSslError(view, handler, error); + InAppWebViewClient.super.onReceivedSslError(view, handler, sslError); } }); } @@ -524,16 +452,15 @@ public class InAppWebViewClient extends WebViewClient { return; } + final String host = request.getHost(); final String protocol = uri.getScheme(); final String realm = null; + final int port = request.getPort(); - Map obj = new HashMap<>(); - obj.put("host", request.getHost()); - obj.put("protocol", protocol); - obj.put("realm", realm); - obj.put("port", request.getPort()); + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + ClientCertChallenge challenge = new ClientCertChallenge(protectionSpace, request.getPrincipals(), request.getKeyTypes()); - channel.invokeMethod("onReceivedClientCertRequest", obj, new MethodChannel.Result() { + channel.invokeMethod("onReceivedClientCertRequest", challenge.toMap(), new MethodChannel.Result() { @Override public void success(Object response) { if (response != null) { @@ -644,9 +571,7 @@ public class InAppWebViewClient extends WebViewClient { if (webView.options.useShouldInterceptRequest) { WebResourceResponse onShouldInterceptResponse = onShouldInterceptRequest(url); - if (onShouldInterceptResponse != null) { - return onShouldInterceptResponse; - } + return onShouldInterceptResponse; } URI uri; @@ -669,7 +594,6 @@ public class InAppWebViewClient extends WebViewClient { if (webView.options.resourceCustomSchemes != null && webView.options.resourceCustomSchemes.contains(scheme)) { final Map obj = new HashMap<>(); obj.put("url", url); - obj.put("scheme", scheme); Util.WaitFlutterResult flutterResult; try { @@ -686,14 +610,14 @@ public class InAppWebViewClient extends WebViewClient { Map res = (Map) flutterResult.result; WebResourceResponse response = null; try { - response = webView.contentBlockerHandler.checkUrl(webView, url, res.get("content-type").toString()); + response = webView.contentBlockerHandler.checkUrl(webView, url, res.get("contentType").toString()); } catch (Exception e) { e.printStackTrace(); } if (response != null) return response; byte[] data = (byte[]) res.get("data"); - return new WebResourceResponse(res.get("content-type").toString(), res.get("content-encoding").toString(), new ByteArrayInputStream(data)); + return new WebResourceResponse(res.get("contentType").toString(), res.get("contentEncoding").toString(), new ByteArrayInputStream(data)); } } @@ -717,9 +641,7 @@ public class InAppWebViewClient extends WebViewClient { if (webView.options.useShouldInterceptRequest) { WebResourceResponse onShouldInterceptResponse = onShouldInterceptRequest(request); - if (onShouldInterceptResponse != null) { - return onShouldInterceptResponse; - } + return onShouldInterceptResponse; } return shouldInterceptRequest(view, url); @@ -729,9 +651,9 @@ public class InAppWebViewClient extends WebViewClient { String url = request instanceof String ? (String) request : null; String method = "GET"; Map headers = null; - Boolean hasGesture = false; - Boolean isForMainFrame = true; - Boolean isRedirect = false; + boolean hasGesture = false; + boolean isForMainFrame = true; + boolean isRedirect = false; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && request instanceof WebResourceRequest) { WebResourceRequest webResourceRequest = (WebResourceRequest) request; @@ -869,12 +791,8 @@ public class InAppWebViewClient extends WebViewClient { } public void dispose() { - channel.setMethodCallHandler(null); - if (inAppBrowserActivity != null) { - inAppBrowserActivity = null; - } - if (flutterWebView != null) { - flutterWebView = null; + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate = null; } } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewOptions.java similarity index 99% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewOptions.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewOptions.java index 96ad6ffc..ad5eebe0 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewOptions.java @@ -1,11 +1,11 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.os.Build; -import android.util.Log; import android.view.View; import android.webkit.WebSettings; import com.pichillilorenzo.flutter_inappwebview.Options; +import com.pichillilorenzo.flutter_inappwebview.types.PreferredContentModeOptionType; import java.util.ArrayList; import java.util.HashMap; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewRenderProcessClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java similarity index 80% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewRenderProcessClient.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java index dd147b15..5dcf80db 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewRenderProcessClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewRenderProcessClient.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.util.Log; import android.webkit.WebView; @@ -9,8 +9,6 @@ import androidx.webkit.WebViewFeature; import androidx.webkit.WebViewRenderProcess; import androidx.webkit.WebViewRenderProcessClient; -import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivity; - import java.util.HashMap; import java.util.Map; @@ -19,17 +17,12 @@ import io.flutter.plugin.common.MethodChannel; public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient { protected static final String LOG_TAG = "IAWRenderProcessClient"; - private FlutterWebView flutterWebView; - private InAppBrowserActivity inAppBrowserActivity; - public MethodChannel channel; + private final MethodChannel channel; - public InAppWebViewRenderProcessClient(Object obj) { + public InAppWebViewRenderProcessClient(MethodChannel channel) { super(); - if (obj instanceof InAppBrowserActivity) - this.inAppBrowserActivity = (InAppBrowserActivity) obj; - else if (obj instanceof FlutterWebView) - this.flutterWebView = (FlutterWebView) obj; - this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; + + this.channel = channel; } @Override @@ -95,4 +88,8 @@ public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient } }); } + + void dispose() { + + } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InputAwareWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InputAwareWebView.java similarity index 99% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InputAwareWebView.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InputAwareWebView.java index 6a69a084..ea2b98f7 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InputAwareWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InputAwareWebView.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import static android.content.Context.INPUT_METHOD_SERVICE; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ThreadedInputConnectionProxyAdapterView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java similarity index 97% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ThreadedInputConnectionProxyAdapterView.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java index e2b7b48b..d0d71692 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/ThreadedInputConnectionProxyAdapterView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.in_app_webview; import android.os.Handler; import android.os.IBinder; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/ConsoleLogJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/ConsoleLogJS.java new file mode 100644 index 00000000..1f00c825 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/ConsoleLogJS.java @@ -0,0 +1,41 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class ConsoleLogJS { + public static final String CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT"; + public static final PluginScript CONSOLE_LOG_JS_PLUGIN_SCRIPT = new PluginScript( + ConsoleLogJS.CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + ConsoleLogJS.CONSOLE_LOG_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true + ); + + public static final String CONSOLE_LOG_JS_SOURCE = "(function(console) {" + + " var oldLogs = {" + + " 'log': console.log," + + " 'debug': console.debug," + + " 'error': console.error," + + " 'info': console.info," + + " 'warn': console.warn" + + " };" + + " for (var k in oldLogs) {" + + " (function(oldLog) {" + + " console[oldLog] = function() {" + + " var message = '';" + + " for (var i in arguments) {" + + " if (message == '') {" + + " message += arguments[i];" + + " }" + + " else {" + + " message += ' ' + arguments[i];" + + " }" + + " }" + + " oldLogs[oldLog].call(console, message);" + + " }" + + " })(k);" + + " }" + + "})(window.console);"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptAjaxRequestJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptAjaxRequestJS.java new file mode 100644 index 00000000..dba3246e --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptAjaxRequestJS.java @@ -0,0 +1,232 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class InterceptAjaxRequestJS { + + public static final String INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptAjaxRequest"; + public static final PluginScript INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript( + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true + ); + + public static final String INTERCEPT_AJAX_REQUEST_JS_SOURCE = "(function(ajax) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " = true;" + + " var send = ajax.prototype.send;" + + " var open = ajax.prototype.open;" + + " var setRequestHeader = ajax.prototype.setRequestHeader;" + + " ajax.prototype._flutter_inappwebview_url = null;" + + " ajax.prototype._flutter_inappwebview_method = null;" + + " ajax.prototype._flutter_inappwebview_isAsync = null;" + + " ajax.prototype._flutter_inappwebview_user = null;" + + " ajax.prototype._flutter_inappwebview_password = null;" + + " ajax.prototype._flutter_inappwebview_password = null;" + + " ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false;" + + " ajax.prototype._flutter_inappwebview_request_headers = {};" + + " function convertRequestResponse(request, callback) {" + + " if (request.response != null && request.responseType != null) {" + + " switch (request.responseType) {" + + " case 'arraybuffer':" + + " callback(new Uint8Array(request.response));" + + " return;" + + " case 'blob':" + + " const reader = new FileReader();" + + " reader.addEventListener('loadend', function() { " + + " callback(new Uint8Array(reader.result));" + + " });" + + " reader.readAsArrayBuffer(blob);" + + " return;" + + " case 'document':" + + " callback(request.response.documentElement.outerHTML);" + + " return;" + + " case 'json':" + + " callback(request.response);" + + " return;" + + " };" + + " }" + + " callback(null);" + + " };" + + " ajax.prototype.open = function(method, url, isAsync, user, password) {" + + " isAsync = (isAsync != null) ? isAsync : true;" + + " this._flutter_inappwebview_url = url;" + + " this._flutter_inappwebview_method = method;" + + " this._flutter_inappwebview_isAsync = isAsync;" + + " this._flutter_inappwebview_user = user;" + + " this._flutter_inappwebview_password = password;" + + " this._flutter_inappwebview_request_headers = {};" + + " open.call(this, method, url, isAsync, user, password);" + + " };" + + " ajax.prototype.setRequestHeader = function(header, value) {" + + " this._flutter_inappwebview_request_headers[header] = value;" + + " setRequestHeader.call(this, header, value);" + + " };" + + " function handleEvent(e) {" + + " var self = this;" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" + + " var headers = this.getAllResponseHeaders();" + + " var responseHeaders = {};" + + " if (headers != null) {" + + " var arr = headers.trim().split(/[\\r\\n]+/);" + + " arr.forEach(function (line) {" + + " var parts = line.split(': ');" + + " var header = parts.shift();" + + " var value = parts.join(': ');" + + " responseHeaders[header] = value;" + + " });" + + " }" + + " convertRequestResponse(this, function(response) {" + + " var ajaxRequest = {" + + " method: self._flutter_inappwebview_method," + + " url: self._flutter_inappwebview_url," + + " isAsync: self._flutter_inappwebview_isAsync," + + " user: self._flutter_inappwebview_user," + + " password: self._flutter_inappwebview_password," + + " withCredentials: self.withCredentials," + + " headers: self._flutter_inappwebview_request_headers," + + " readyState: self.readyState," + + " status: self.status," + + " responseURL: self.responseURL," + + " responseType: self.responseType," + + " response: response," + + " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + + " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + + " statusText: self.statusText," + + " responseHeaders, responseHeaders," + + " event: {" + + " type: e.type," + + " loaded: e.loaded," + + " lengthComputable: e.lengthComputable," + + " total: e.total" + + " }" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxProgress', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " }" + + " });" + + " });" + + " }" + + " };" + + " ajax.prototype.send = function(data) {" + + " var self = this;" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" + + " if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) {" + + " this._flutter_inappwebview_already_onreadystatechange_wrapped = true;" + + " var onreadystatechange = this.onreadystatechange;" + + " this.onreadystatechange = function() {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" + + " var headers = this.getAllResponseHeaders();" + + " var responseHeaders = {};" + + " if (headers != null) {" + + " var arr = headers.trim().split(/[\\r\\n]+/);" + + " arr.forEach(function (line) {" + + " var parts = line.split(': ');" + + " var header = parts.shift();" + + " var value = parts.join(': ');" + + " responseHeaders[header] = value;" + + " });" + + " }" + + " convertRequestResponse(this, function(response) {" + + " var ajaxRequest = {" + + " method: self._flutter_inappwebview_method," + + " url: self._flutter_inappwebview_url," + + " isAsync: self._flutter_inappwebview_isAsync," + + " user: self._flutter_inappwebview_user," + + " password: self._flutter_inappwebview_password," + + " withCredentials: self.withCredentials," + + " headers: self._flutter_inappwebview_request_headers," + + " readyState: self.readyState," + + " status: self.status," + + " responseURL: self.responseURL," + + " responseType: self.responseType," + + " response: response," + + " responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," + + " responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," + + " statusText: self.statusText," + + " responseHeaders: responseHeaders" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " }" + + " if (onreadystatechange != null) {" + + " onreadystatechange();" + + " }" + + " });" + + " });" + + " } else if (onreadystatechange != null) {" + + " onreadystatechange();" + + " }" + + " };" + + " }" + + " this.addEventListener('loadstart', handleEvent);" + + " this.addEventListener('load', handleEvent);" + + " this.addEventListener('loadend', handleEvent);" + + " this.addEventListener('progress', handleEvent);" + + " this.addEventListener('error', handleEvent);" + + " this.addEventListener('abort', handleEvent);" + + " this.addEventListener('timeout', handleEvent);" + + " var ajaxRequest = {" + + " data: data," + + " method: this._flutter_inappwebview_method," + + " url: this._flutter_inappwebview_url," + + " isAsync: this._flutter_inappwebview_isAsync," + + " user: this._flutter_inappwebview_user," + + " password: this._flutter_inappwebview_password," + + " withCredentials: this.withCredentials," + + " headers: this._flutter_inappwebview_request_headers," + + " responseType: this.responseType" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result.action) {" + + " case 0:" + + " self.abort();" + + " return;" + + " };" + + " data = result.data;" + + " self.withCredentials = result.withCredentials;" + + " if (result.responseType != null) {" + + " self.responseType = result.responseType;" + + " };" + + " for (var header in result.headers) {" + + " var value = result.headers[header];" + + " var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header];" + + " if (flutter_inappwebview_value == null) {" + + " self._flutter_inappwebview_request_headers[header] = value;" + + " } else {" + + " self._flutter_inappwebview_request_headers[header] += ', ' + value;" + + " }" + + " setRequestHeader.call(self, header, value);" + + " };" + + " if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) {" + + " self.abort();" + + " self.open(result.method, result.url, result.isAsync, result.user, result.password);" + + " return;" + + " }" + + " }" + + " send.call(self, data);" + + " });" + + " } else {" + + " send.call(this, data);" + + " }" + + " };" + + "})(window.XMLHttpRequest);"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptFetchRequestJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptFetchRequestJS.java new file mode 100644 index 00000000..bac04c0d --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/InterceptFetchRequestJS.java @@ -0,0 +1,200 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class InterceptFetchRequestJS { + + public static final String INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptFetchRequest"; + public static final PluginScript INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript( + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true + ); + + public static final String INTERCEPT_FETCH_REQUEST_JS_SOURCE = "(function(fetch) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " = true;" + + " if (fetch == null) {" + + " return;" + + " }" + + " function convertHeadersToJson(headers) {" + + " var headersObj = {};" + + " for (var header of headers.keys()) {" + + " var value = headers.get(header);" + + " headersObj[header] = value;" + + " }" + + " return headersObj;" + + " }" + + " function convertJsonToHeaders(headersJson) {" + + " return new Headers(headersJson);" + + " }" + + " function convertBodyToArray(body) {" + + " return new Response(body).arrayBuffer().then(function(arrayBuffer) {" + + " var arr = Array.from(new Uint8Array(arrayBuffer));" + + " return arr;" + + " })" + + " }" + + " function convertArrayIntBodyToUint8Array(arrayIntBody) {" + + " return new Uint8Array(arrayIntBody);" + + " }" + + " function convertCredentialsToJson(credentials) {" + + " var credentialsObj = {};" + + " if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" + + " credentialsObj.type = credentials.type;" + + " credentialsObj.id = credentials.id;" + + " credentialsObj.name = credentials.name;" + + " credentialsObj.protocol = credentials.protocol;" + + " credentialsObj.provider = credentials.provider;" + + " credentialsObj.iconURL = credentials.iconURL;" + + " } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" + + " credentialsObj.type = credentials.type;" + + " credentialsObj.id = credentials.id;" + + " credentialsObj.name = credentials.name;" + + " credentialsObj.password = credentials.password;" + + " credentialsObj.iconURL = credentials.iconURL;" + + " } else {" + + " credentialsObj.type = 'default';" + + " credentialsObj.value = credentials;" + + " }" + + " }" + + " function convertJsonToCredential(credentialsJson) {" + + " var credentials;" + + " if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" + + " credentials = new FederatedCredential({" + + " id: credentialsJson.id," + + " name: credentialsJson.name," + + " protocol: credentialsJson.protocol," + + " provider: credentialsJson.provider," + + " iconURL: credentialsJson.iconURL" + + " });" + + " } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" + + " credentials = new PasswordCredential({" + + " id: credentialsJson.id," + + " name: credentialsJson.name," + + " password: credentialsJson.password," + + " iconURL: credentialsJson.iconURL" + + " });" + + " } else {" + + " credentials = credentialsJson;" + + " }" + + " return credentials;" + + " }" + + " window.fetch = async function(resource, init) {" + + " var w = (window.top == null || window.top === window) ? window : window.top;" + + " if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == true) {" + + " var fetchRequest = {" + + " url: null," + + " method: null," + + " headers: null," + + " body: null," + + " mode: null," + + " credentials: null," + + " cache: null," + + " redirect: null," + + " referrer: null," + + " referrerPolicy: null," + + " integrity: null," + + " keepalive: null" + + " };" + + " if (resource instanceof Request) {" + + " fetchRequest.url = resource.url;" + + " fetchRequest.method = resource.method;" + + " fetchRequest.headers = resource.headers;" + + " fetchRequest.body = resource.body;" + + " fetchRequest.mode = resource.mode;" + + " fetchRequest.credentials = resource.credentials;" + + " fetchRequest.cache = resource.cache;" + + " fetchRequest.redirect = resource.redirect;" + + " fetchRequest.referrer = resource.referrer;" + + " fetchRequest.referrerPolicy = resource.referrerPolicy;" + + " fetchRequest.integrity = resource.integrity;" + + " fetchRequest.keepalive = resource.keepalive;" + + " } else {" + + " fetchRequest.url = resource;" + + " if (init != null) {" + + " fetchRequest.method = init.method;" + + " fetchRequest.headers = init.headers;" + + " fetchRequest.body = init.body;" + + " fetchRequest.mode = init.mode;" + + " fetchRequest.credentials = init.credentials;" + + " fetchRequest.cache = init.cache;" + + " fetchRequest.redirect = init.redirect;" + + " fetchRequest.referrer = init.referrer;" + + " fetchRequest.referrerPolicy = init.referrerPolicy;" + + " fetchRequest.integrity = init.integrity;" + + " fetchRequest.keepalive = init.keepalive;" + + " }" + + " }" + + " if (fetchRequest.headers instanceof Headers) {" + + " fetchRequest.headers = convertHeadersToJson(fetchRequest.headers);" + + " }" + + " fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials);" + + " return convertBodyToArray(fetchRequest.body).then(function(body) {" + + " fetchRequest.body = body;" + + " return window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {" + + " if (result != null) {" + + " switch (result.action) {" + + " case 0:" + + " var controller = new AbortController();" + + " if (init != null) {" + + " init.signal = controller.signal;" + + " } else {" + + " init = {" + + " signal: controller.signal" + + " };" + + " }" + + " controller.abort();" + + " break;" + + " }" + + " resource = (result.url != null) ? result.url : resource;" + + " if (init == null) {" + + " init = {};" + + " }" + + " if (result.method != null && result.method.length > 0) {" + + " init.method = result.method;" + + " }" + + " if (result.headers != null && Object.keys(result.headers).length > 0) {" + + " init.headers = convertJsonToHeaders(result.headers);" + + " }" + + " if (result.body != null && result.body.length > 0) {" + + " init.body = convertArrayIntBodyToUint8Array(result.body);" + + " }" + + " if (result.mode != null && result.mode.length > 0) {" + + " init.mode = result.mode;" + + " }" + + " if (result.credentials != null) {" + + " init.credentials = convertJsonToCredential(result.credentials);" + + " }" + + " if (result.cache != null && result.cache.length > 0) {" + + " init.cache = result.cache;" + + " }" + + " if (result.redirect != null && result.redirect.length > 0) {" + + " init.redirect = result.redirect;" + + " }" + + " if (result.referrer != null && result.referrer.length > 0) {" + + " init.referrer = result.referrer;" + + " }" + + " if (result.referrerPolicy != null && result.referrerPolicy.length > 0) {" + + " init.referrerPolicy = result.referrerPolicy;" + + " }" + + " if (result.integrity != null && result.integrity.length > 0) {" + + " init.integrity = result.integrity;" + + " }" + + " if (result.keepalive != null) {" + + " init.keepalive = result.keepalive;" + + " }" + + " return fetch(resource, init);" + + " }" + + " return fetch(resource, init);" + + " });" + + " });" + + " } else {" + + " return fetch(resource, init);" + + " }" + + " };" + + "})(window.fetch);"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/JavaScriptBridgeJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/JavaScriptBridgeJS.java new file mode 100644 index 00000000..834789aa --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/JavaScriptBridgeJS.java @@ -0,0 +1,42 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class JavaScriptBridgeJS { + public static final String JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview"; + public static final String JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT"; + public static final PluginScript JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = new PluginScript( + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true + ); + + public static final String JAVASCRIPT_BRIDGE_JS_SOURCE = "if (window.top == null || window.top === window) {" + + " window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" + + " var _callHandlerID = setTimeout(function(){});" + + " window." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" + + " return new Promise(function(resolve, reject) {" + + " window." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = resolve;" + + " });" + + " };"+ + "} else {" + + " window." + JAVASCRIPT_BRIDGE_NAME + " = {};" + + " window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" + + " var _callHandlerID = setTimeout(function(){});" + + " window.top." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" + + " return new Promise(function(resolve, reject) {" + + " window.top." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = resolve;" + + " });" + + " };"+ + "}"; + + public static final String PLATFORM_READY_JS_SOURCE = "(function() {" + + " if ((window.top == null || window.top === window) && window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady == null) {" + + " window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));" + + " window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady = true;" + + " }" + + "})();"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnLoadResourceJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnLoadResourceJS.java new file mode 100644 index 00000000..e7b3d0bc --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnLoadResourceJS.java @@ -0,0 +1,34 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class OnLoadResourceJS { + public static final String ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT"; + public static final String FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useOnLoadResource"; + public static final PluginScript ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = new PluginScript( + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false + ); + + public static final String ON_LOAD_RESOURCE_JS_SOURCE = "window." + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " = true;" + + "(function() {" + + " var observer = new PerformanceObserver(function(list) {" + + " list.getEntries().forEach(function(entry) {" + + " if (" + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == null || " + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == true) {" + + " var resource = {" + + " 'url': entry.name," + + " 'initiatorType': entry.initiatorType," + + " 'startTime': entry.startTime," + + " 'duration': entry.duration" + + " };" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onLoadResource', resource);" + + " }" + + " });" + + " });" + + " observer.observe({entryTypes: ['resource']});" + + "})();"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowBlurEventJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowBlurEventJS.java new file mode 100644 index 00000000..4854a26d --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowBlurEventJS.java @@ -0,0 +1,21 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class OnWindowBlurEventJS { + public static final String ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT"; + public static final PluginScript ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = new PluginScript( + OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false + ); + + public static final String ON_WINDOW_BLUR_EVENT_JS_SOURCE = "(function(){" + + " window.addEventListener('blur', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowBlur');" + + " });" + + "})();"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowFocusEventJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowFocusEventJS.java new file mode 100644 index 00000000..04e9bc09 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/OnWindowFocusEventJS.java @@ -0,0 +1,21 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class OnWindowFocusEventJS { + public static final String ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT"; + public static final PluginScript ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = new PluginScript( + OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false + ); + + public static final String ON_WINDOW_FOCUS_EVENT_JS_SOURCE = "(function(){" + + " window.addEventListener('focus', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowFocus');" + + " });" + + "})();"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PluginScriptsUtil.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PluginScriptsUtil.java new file mode 100644 index 00000000..e48937d4 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PluginScriptsUtil.java @@ -0,0 +1,82 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class PluginScriptsUtil { + + public static final String VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE"; + public static final String VAR_CONTENT_WORLD_NAME_ARRAY = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME_ARRAY"; + public static final String VAR_CONTENT_WORLD_NAME = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME"; + public static final String VAR_JSON_SOURCE_ENCODED = "$IN_APP_WEBVIEW_JSON_SOURCE_ENCODED"; + public static final String VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES"; + public static final String VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES"; + public static final String VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ"; + public static final String VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY"; + public static final String VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID"; + + public static final String CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE = "(function(obj) {" + + " (async function(" + VAR_FUNCTION_ARGUMENT_NAMES + ") {" + + " " + VAR_FUNCTION_BODY + + " })(" + VAR_FUNCTION_ARGUMENT_VALUES + ").then(function(value) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': value, 'error': null, 'resultUuid': '" + VAR_RESULT_UUID + "'});" + + " }).catch(function(error) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': null, 'error': error + '', 'resultUuid': '" + VAR_RESULT_UUID + "'});" + + " });" + + " return null;" + + "})(" + VAR_FUNCTION_ARGUMENTS_OBJ + ");"; + + public static final String EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE = "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('evaluateJavaScriptWithContentWorld', {'value': eval(" + VAR_PLACEHOLDER_VALUE + "), 'resultUuid': '" + VAR_RESULT_UUID + "'});"; + + public static final String IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE = + "var activeEl = document.activeElement;" + + "var nodeName = (activeEl != null) ? activeEl.nodeName.toLowerCase() : '';" + + "var isActiveElementInputEditable = activeEl != null && " + + "(activeEl.nodeType == 1 && (nodeName == 'textarea' || (nodeName == 'input' && /^(?:text|email|number|search|tel|url|password)$/i.test(activeEl.type != null ? activeEl.type : 'text')))) && " + + "!activeEl.disabled && !activeEl.readOnly;" + + "var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';"; + + // android Workaround to hide context menu when selected text is empty + // and the document active element is not an input element. + public static final String CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE + + " return txt === '' && !isActiveElementEditable;" + + "})();"; + + public static final String GET_SELECTED_TEXT_JS_SOURCE = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + " return txt;" + + "})();"; + + public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME = "CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT"; + public static final PluginScript CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT = new PluginScript( + PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME, + PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false + ); + + // android Workaround to hide context menu when user emit a keydown event + public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE = "(function(){" + + " document.addEventListener('keydown', function(e) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._hideContextMenu();" + + " });" + + "})();"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PrintJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PrintJS.java new file mode 100644 index 00000000..d8f7d18f --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PrintJS.java @@ -0,0 +1,23 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class PrintJS { + public static final String PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT"; + public static final PluginScript PRINT_JS_PLUGIN_SCRIPT = new PluginScript( + PrintJS.PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + PrintJS.PRINT_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false + ); + + public static final String PRINT_JS_SOURCE = "window.print = function() {" + + " if (window.top == null || window.top === window) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onPrint', window.location.href);" + + " } else {" + + " window.top.print();" + + " }" + + "};"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PromisePolyfillJS.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PromisePolyfillJS.java new file mode 100644 index 00000000..7303d032 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/plugin_scripts_js/PromisePolyfillJS.java @@ -0,0 +1,20 @@ +package com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js; + +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; + +public class PromisePolyfillJS { + public static final String PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT"; + public static final PluginScript PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = new PluginScript( + PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + PromisePolyfillJS.PROMISE_POLYFILL_JS_SOURCE, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + true + ); + + // https://github.com/tildeio/rsvp.js + public static final String PROMISE_POLYFILL_JS_SOURCE = "if (window.Promise == null) {" + + " !function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.RSVP={})}(this,function(t){\"use strict\";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if(\"function\"!=typeof r)throw new TypeError(\"Callback must be a function\");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return!e||\"object\"!=typeof e&&\"function\"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if(\"function\"!=typeof e&&null!==e)throw new TypeError(\"Super expression must either be null or a function, not \"+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s toMap() { + List principalList = null; + if (principals != null) { + principalList = new ArrayList<>(); + for (Principal principal : principals) { + principalList.add(principal.getName()); + } + } + + Map challengeMap = super.toMap(); + challengeMap.put("androidPrincipals", principalList); + challengeMap.put("androidKeyTypes", keyTypes != null ? Arrays.asList(keyTypes) : null); + return challengeMap; + } + + @Nullable + public Principal[] getPrincipals() { + return principals; + } + + public void setPrincipals(@Nullable Principal[] principals) { + this.principals = principals; + } + + @Nullable + public String[] getKeyTypes() { + return keyTypes; + } + + public void setKeyTypes(@Nullable String[] keyTypes) { + this.keyTypes = keyTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ClientCertChallenge that = (ClientCertChallenge) o; + + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(principals, that.principals)) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(keyTypes, that.keyTypes); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(principals); + result = 31 * result + Arrays.hashCode(keyTypes); + return result; + } + + @Override + public String toString() { + return "ClientCertChallenge{" + + "principals=" + Arrays.toString(principals) + + ", keyTypes=" + Arrays.toString(keyTypes) + + "} " + super.toString(); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ContentWorld.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ContentWorld.java new file mode 100644 index 00000000..f10a8e58 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ContentWorld.java @@ -0,0 +1,63 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ContentWorld { + @NonNull + private String name; + + public static final ContentWorld PAGE = new ContentWorld("page"); + public static final ContentWorld DEFAULT_CLIENT = new ContentWorld("defaultClient"); + + private ContentWorld(@NonNull String name) { + this.name = name; + } + + public static ContentWorld world(@NonNull String name) { + return new ContentWorld(name); + } + + @Nullable + public static ContentWorld fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + assert name != null; + return new ContentWorld(name); + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentWorld that = (ContentWorld) o; + + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "ContentWorld{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CreateWindowAction.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CreateWindowAction.java new file mode 100644 index 00000000..854bef06 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CreateWindowAction.java @@ -0,0 +1,69 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import java.util.Map; + +public class CreateWindowAction extends NavigationAction { + int windowId; + boolean isDialog; + + public CreateWindowAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect, int windowId, boolean isDialog) { + super(request, isForMainFrame, hasGesture, isRedirect); + this.windowId = windowId; + this.isDialog = isDialog; + } + + public Map toMap() { + Map createWindowActionMap = super.toMap(); + createWindowActionMap.put("windowId", windowId); + createWindowActionMap.put("androidIsDialog", isDialog); + return createWindowActionMap; + } + + public int getWindowId() { + return windowId; + } + + public void setWindowId(int windowId) { + this.windowId = windowId; + } + + public boolean isDialog() { + return isDialog; + } + + public void setDialog(boolean dialog) { + isDialog = dialog; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + CreateWindowAction that = (CreateWindowAction) o; + + if (windowId != that.windowId) return false; + return isDialog == that.isDialog; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + windowId; + result = 31 * result + (isDialog ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "CreateWindowAction{" + + "windowId=" + windowId + + ", isDialog=" + isDialog + + ", request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/HttpAuthenticationChallenge.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/HttpAuthenticationChallenge.java new file mode 100644 index 00000000..5db545de --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/HttpAuthenticationChallenge.java @@ -0,0 +1,69 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class HttpAuthenticationChallenge extends URLAuthenticationChallenge { + private int previousFailureCount; + @Nullable + URLCredential proposedCredential; + + public HttpAuthenticationChallenge(URLProtectionSpace protectionSpace, int previousFailureCount, @Nullable URLCredential proposedCredential) { + super(protectionSpace); + this.previousFailureCount = previousFailureCount; + this.proposedCredential = proposedCredential; + } + + public Map toMap() { + Map challengeMap = super.toMap(); + challengeMap.put("previousFailureCount", previousFailureCount); + challengeMap.put("proposedCredential", (proposedCredential != null) ? proposedCredential.toMap() : null); + return challengeMap; + } + + public int getPreviousFailureCount() { + return previousFailureCount; + } + + public void setPreviousFailureCount(int previousFailureCount) { + this.previousFailureCount = previousFailureCount; + } + + @Nullable + public URLCredential getProposedCredential() { + return proposedCredential; + } + + public void setProposedCredential(@Nullable URLCredential proposedCredential) { + this.proposedCredential = proposedCredential; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + HttpAuthenticationChallenge that = (HttpAuthenticationChallenge) o; + + if (previousFailureCount != that.previousFailureCount) return false; + return proposedCredential != null ? proposedCredential.equals(that.proposedCredential) : that.proposedCredential == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + previousFailureCount; + result = 31 * result + (proposedCredential != null ? proposedCredential.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpAuthenticationChallenge{" + + "previousFailureCount=" + previousFailureCount + + ", proposedCredential=" + proposedCredential + + "} " + super.toString(); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java new file mode 100644 index 00000000..31e82563 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java @@ -0,0 +1,91 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import java.util.HashMap; +import java.util.Map; + +public class NavigationAction { + URLRequest request; + boolean isForMainFrame; + boolean hasGesture; + boolean isRedirect; + + public NavigationAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect) { + this.request = request; + this.isForMainFrame = isForMainFrame; + this.hasGesture = hasGesture; + this.isRedirect = isRedirect; + } + + public Map toMap() { + Map navigationActionMap = new HashMap<>(); + navigationActionMap.put("request", request.toMap()); + navigationActionMap.put("isForMainFrame", isForMainFrame); + navigationActionMap.put("hasGesture", hasGesture); + navigationActionMap.put("isRedirect", isRedirect); + return navigationActionMap; + } + + public URLRequest getRequest() { + return request; + } + + public void setRequest(URLRequest request) { + this.request = request; + } + + public boolean isForMainFrame() { + return isForMainFrame; + } + + public void setForMainFrame(boolean forMainFrame) { + isForMainFrame = forMainFrame; + } + + public boolean isHasGesture() { + return hasGesture; + } + + public void setHasGesture(boolean hasGesture) { + this.hasGesture = hasGesture; + } + + public boolean isRedirect() { + return isRedirect; + } + + public void setRedirect(boolean redirect) { + isRedirect = redirect; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NavigationAction that = (NavigationAction) o; + + if (isForMainFrame != that.isForMainFrame) return false; + if (hasGesture != that.hasGesture) return false; + if (isRedirect != that.isRedirect) return false; + return request.equals(that.request); + } + + @Override + public int hashCode() { + int result = request.hashCode(); + result = 31 * result + (isForMainFrame ? 1 : 0); + result = 31 * result + (hasGesture ? 1 : 0); + result = 31 * result + (isRedirect ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "NavigationAction{" + + "request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationActionPolicy.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationActionPolicy.java new file mode 100644 index 00000000..f75e13a2 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationActionPolicy.java @@ -0,0 +1,33 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +public enum NavigationActionPolicy { + CANCEL(0), + ALLOW(1); + + private final int value; + + private NavigationActionPolicy(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static NavigationActionPolicy fromValue(int value) { + for( NavigationActionPolicy type : NavigationActionPolicy.values()) { + if(value == type.value) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int rawValue() { + return this.value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PluginScript.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PluginScript.java new file mode 100644 index 00000000..e7fe563c --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PluginScript.java @@ -0,0 +1,46 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class PluginScript extends UserScript { + private boolean requiredInAllContentWorlds; + + public PluginScript(@Nullable String groupName, @NonNull String source, @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld, boolean requiredInAllContentWorlds) { + super(groupName, source, injectionTime, contentWorld); + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + public boolean isRequiredInAllContentWorlds() { + return requiredInAllContentWorlds; + } + + public void setRequiredInAllContentWorlds(boolean requiredInAllContentWorlds) { + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + PluginScript that = (PluginScript) o; + + return requiredInAllContentWorlds == that.requiredInAllContentWorlds; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (requiredInAllContentWorlds ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PluginScript{" + + "requiredInContentWorld=" + requiredInAllContentWorlds + + "} " + super.toString(); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/PreferredContentModeOptionType.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PreferredContentModeOptionType.java similarity index 91% rename from android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/PreferredContentModeOptionType.java rename to android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PreferredContentModeOptionType.java index 5905152e..a284c69c 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/PreferredContentModeOptionType.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/PreferredContentModeOptionType.java @@ -1,4 +1,4 @@ -package com.pichillilorenzo.flutter_inappwebview.InAppWebView; +package com.pichillilorenzo.flutter_inappwebview.types; public enum PreferredContentModeOptionType { RECOMMENDED (0), diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ServerTrustChallenge.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ServerTrustChallenge.java new file mode 100644 index 00000000..c7212c27 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/ServerTrustChallenge.java @@ -0,0 +1,12 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +public class ServerTrustChallenge extends URLAuthenticationChallenge { + public ServerTrustChallenge(URLProtectionSpace protectionSpace) { + super(protectionSpace); + } + + @Override + public String toString() { + return "ServerTrustChallenge{} " + super.toString(); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslCertificateExt.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslCertificateExt.java new file mode 100644 index 00000000..e8736b24 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslCertificateExt.java @@ -0,0 +1,77 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.net.http.SslCertificate; +import android.os.Build; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.Util; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +public class SslCertificateExt extends SslCertificate { + + private SslCertificateExt(X509Certificate certificate) { + super(certificate); + } + + @Nullable + static public Map toMap(@Nullable SslCertificate sslCertificate) { + if (sslCertificate == null) { + return null; + } + + DName issuedByName = sslCertificate.getIssuedBy(); + Map issuedBy = null; + if (issuedByName != null) { + issuedBy = new HashMap<>(); + issuedBy.put("CName", issuedByName.getCName()); + issuedBy.put("DName", issuedByName.getDName()); + issuedBy.put("OName", issuedByName.getOName()); + issuedBy.put("UName", issuedByName.getUName()); + } + + DName issuedToName = sslCertificate.getIssuedTo(); + Map issuedTo = null; + if (issuedToName != null) { + issuedTo = new HashMap<>(); + issuedTo.put("CName", issuedToName.getCName()); + issuedTo.put("DName", issuedToName.getDName()); + issuedTo.put("OName", issuedToName.getOName()); + issuedTo.put("UName", issuedToName.getUName()); + } + + byte[] x509CertificateData = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + X509Certificate certificate = sslCertificate.getX509Certificate(); + if (certificate != null) { + x509CertificateData = certificate.getEncoded(); + } + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } else { + try { + x509CertificateData = Util.getX509CertFromSslCertHack(sslCertificate).getEncoded(); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } + + long validNotAfterDate = sslCertificate.getValidNotAfterDate().getTime(); + long validNotBeforeDate = sslCertificate.getValidNotBeforeDate().getTime(); + + Map sslCertificateMap = new HashMap<>(); + sslCertificateMap.put("issuedBy", issuedBy); + sslCertificateMap.put("issuedTo", issuedTo); + sslCertificateMap.put("validNotAfterDate", validNotAfterDate); + sslCertificateMap.put("validNotBeforeDate", validNotBeforeDate); + sslCertificateMap.put("x509Certificate", x509CertificateData); + return sslCertificateMap; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslErrorExt.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslErrorExt.java new file mode 100644 index 00000000..ec7d03a4 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/SslErrorExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class SslErrorExt extends SslError { + + private SslErrorExt(int error, SslCertificate certificate, String url) { + super(error, certificate, url); + } + + @Nullable + static public Map toMap(SslError sslError) { + if (sslError == null) { + return null; + } + + int primaryError = sslError.getPrimaryError(); + + String message; + switch (primaryError) { + case SslError.SSL_DATE_INVALID: + message = "The date of the certificate is invalid"; + break; + case SslError.SSL_EXPIRED: + message = "The certificate has expired"; + break; + case SslError.SSL_IDMISMATCH: + message = "Hostname mismatch"; + break; + case SslError.SSL_INVALID: + message = "A generic error occurred"; + break; + case SslError.SSL_NOTYETVALID: + message = "The certificate is not yet valid"; + break; + case SslError.SSL_UNTRUSTED: + message = "The certificate authority is not trusted"; + break; + default: + message = null; + break; + } + + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("androidError", primaryError); + urlProtectionSpaceMap.put("message", message); + return urlProtectionSpaceMap; + } + +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLAuthenticationChallenge.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLAuthenticationChallenge.java new file mode 100644 index 00000000..4348f6c4 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLAuthenticationChallenge.java @@ -0,0 +1,48 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import java.util.HashMap; +import java.util.Map; + +public class URLAuthenticationChallenge { + private URLProtectionSpace protectionSpace; + + public URLAuthenticationChallenge(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + public Map toMap() { + Map challengeMap = new HashMap<>(); + challengeMap.put("protectionSpace", protectionSpace.toMap()); + return challengeMap; + } + + public URLProtectionSpace getProtectionSpace() { + return protectionSpace; + } + + public void setProtectionSpace(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLAuthenticationChallenge challenge = (URLAuthenticationChallenge) o; + + return protectionSpace.equals(challenge.protectionSpace); + } + + @Override + public int hashCode() { + return protectionSpace.hashCode(); + } + + @Override + public String toString() { + return "URLAuthenticationChallenge{" + + "protectionSpace=" + protectionSpace + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLCredential.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLCredential.java new file mode 100644 index 00000000..8251d84c --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLCredential.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLCredential { + @Nullable + private Long id; + @Nullable + private String username; + @Nullable + private String password; + @Nullable + private Long protectionSpaceId; + + public URLCredential(@Nullable String username, @Nullable String password) { + this.username = username; + this.password = password; + } + + public URLCredential (@Nullable Long id, @NonNull String username, @NonNull String password, @Nullable Long protectionSpaceId) { + this.id = id; + this.username = username; + this.password = password; + this.protectionSpaceId = protectionSpaceId; + } + + public Map toMap() { + Map urlCredentialMap = new HashMap<>(); + urlCredentialMap.put("username", username); + urlCredentialMap.put("password", password); + return urlCredentialMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + @Nullable + public String getUsername() { + return username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + @Nullable + public String getPassword() { + return password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + @Nullable + public Long getProtectionSpaceId() { + return protectionSpaceId; + } + + public void setProtectionSpaceId(@Nullable Long protectionSpaceId) { + this.protectionSpaceId = protectionSpaceId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLCredential that = (URLCredential) o; + + if (username != null ? !username.equals(that.username) : that.username != null) return false; + return password != null ? password.equals(that.password) : that.password == null; + } + + @Override + public int hashCode() { + int result = username != null ? username.hashCode() : 0; + result = 31 * result + (password != null ? password.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLCredential{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLProtectionSpace.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLProtectionSpace.java new file mode 100644 index 00000000..d177ad16 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLProtectionSpace.java @@ -0,0 +1,150 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLProtectionSpace { + @Nullable + private Long id; + private String host; + private String protocol; + @Nullable + private String realm; + private int port; + @Nullable + private SslCertificate sslCertificate; + @Nullable + private SslError sslError; + + public URLProtectionSpace(String host, String protocol, @Nullable String realm, int port, @Nullable SslCertificate sslCertificate, @Nullable SslError sslError) { + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + this.sslCertificate = sslCertificate; + this.sslError = sslError; + } + + public URLProtectionSpace(@Nullable Long id, String host, String protocol, @Nullable String realm, int port) { + this.id = id; + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + } + + public Map toMap() { + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("host", host); + urlProtectionSpaceMap.put("protocol", protocol); + urlProtectionSpaceMap.put("realm", realm); + urlProtectionSpaceMap.put("port", port); + urlProtectionSpaceMap.put("sslCertificate", SslCertificateExt.toMap(sslCertificate)); + urlProtectionSpaceMap.put("sslError", SslErrorExt.toMap(sslError)); + return urlProtectionSpaceMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Nullable + public String getRealm() { + return realm; + } + + public void setRealm(@Nullable String realm) { + this.realm = realm; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + @Nullable + public SslCertificate getSslCertificate() { + return sslCertificate; + } + + public void setSslCertificate(@Nullable SslCertificate sslCertificateExt) { + this.sslCertificate = sslCertificateExt; + } + + @Nullable + public SslError getSslError() { + return sslError; + } + + public void setSslError(@Nullable SslError sslError) { + this.sslError = sslError; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLProtectionSpace that = (URLProtectionSpace) o; + + if (port != that.port) return false; + if (!host.equals(that.host)) return false; + if (!protocol.equals(that.protocol)) return false; + if (realm != null ? !realm.equals(that.realm) : that.realm != null) return false; + if (sslCertificate != null ? !sslCertificate.equals(that.sslCertificate) : that.sslCertificate != null) + return false; + return sslError != null ? sslError.equals(that.sslError) : that.sslError == null; + } + + @Override + public int hashCode() { + int result = host.hashCode(); + result = 31 * result + protocol.hashCode(); + result = 31 * result + (realm != null ? realm.hashCode() : 0); + result = 31 * result + port; + result = 31 * result + (sslCertificate != null ? sslCertificate.hashCode() : 0); + result = 31 * result + (sslError != null ? sslError.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLProtectionSpace{" + + "host='" + host + '\'' + + ", protocol='" + protocol + '\'' + + ", realm='" + realm + '\'' + + ", port=" + port + + ", sslCertificate=" + sslCertificate + + ", sslError=" + sslError + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLRequest.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLRequest.java new file mode 100644 index 00000000..5dd33c14 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/URLRequest.java @@ -0,0 +1,115 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class URLRequest { + @NonNull + private String url; + @Nullable + private String method; + @Nullable + private byte[] body; + @Nullable + private Map headers; + + public URLRequest(@NonNull String url, @Nullable String method, @Nullable byte[] body, @Nullable Map headers) { + this.url = url; + this.method = method; + this.body = body; + this.headers = headers; + } + + @Nullable + public static URLRequest fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String url = (String) map.get("url"); + String method = (String) map.get("method"); + byte[] body = (byte[]) map.get("body"); + Map headers = (Map) map.get("headers"); + assert url != null; + return new URLRequest(url, method, body, headers); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("url", url); + urlRequestMap.put("method", method); + urlRequestMap.put("body", body); + return urlRequestMap; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + @Nullable + public String getMethod() { + return method; + } + + public void setMethod(@Nullable String method) { + this.method = method; + } + + @Nullable + public byte[] getBody() { + return body; + } + + public void setBody(@Nullable byte[] body) { + this.body = body; + } + + @Nullable + public Map getHeaders() { + return headers; + } + + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLRequest that = (URLRequest) o; + + if (!url.equals(that.url)) return false; + if (method != null ? !method.equals(that.method) : that.method != null) return false; + if (!Arrays.equals(body, that.body)) return false; + return headers != null ? headers.equals(that.headers) : that.headers == null; + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + (method != null ? method.hashCode() : 0); + result = 31 * result + Arrays.hashCode(body); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLRequest{" + + "url='" + url + '\'' + + ", method='" + method + '\'' + + ", body=" + Arrays.toString(body) + + ", headers=" + headers + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserContentController.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserContentController.java new file mode 100644 index 00000000..ee1c4f40 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserContentController.java @@ -0,0 +1,364 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.Util; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PluginScriptsUtil; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class UserContentController { + protected static final String LOG_TAG = "UserContentController"; + + @NonNull + private final Set contentWorlds = new HashSet() {{ + add(ContentWorld.PAGE); + }}; + + @NonNull + private final Map> userOnlyScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + @NonNull + private final Map> pluginScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + + public UserContentController() { + } + + public String generateWrappedCodeForDocumentStart() { + return Util.replaceAll( + DOCUMENT_READY_WRAPPER_JS_SOURCE, + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, + generateCodeForDocumentStart()); + } + + public String generateWrappedCodeForDocumentEnd() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_END; + // try to reload scripts if they were not loaded during the AT_DOCUMENT_START event + String js = generateCodeForDocumentStart(); + js += generatePluginScriptsCodeAt(injectionTime); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + private String generateCodeForDocumentStart() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_START; + String js = ""; + js += generatePluginScriptsCodeAt(injectionTime); + js += generateContentWorldsCreatorCode(); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + private String generateContentWorldsCreatorCode() { + if (this.contentWorlds.size() == 1) { + return ""; + } + + StringBuilder source = new StringBuilder(); + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + source.append(script.getSource()); + } + List contentWorldsNames = new ArrayList<>(); + for (ContentWorld contentWorld : this.contentWorlds) { + if (contentWorld.equals(ContentWorld.PAGE)) { + continue; + } + contentWorldsNames.add("'" + escapeContentWorldName(contentWorld.getName()) + "'"); + } + + return CONTENT_WORLDS_GENERATOR_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY, TextUtils.join(", ", contentWorldsNames)) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source.toString())); + + } + + private String generatePluginScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getPluginScriptsAt(injectionTime); + for (PluginScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + private String generateUserOnlyScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getUserOnlyScriptsAt(injectionTime); + for (UserScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + public String generateCodeForScriptEvaluation(String source, @Nullable ContentWorld contentWorld) { + if (contentWorld != null && !contentWorld.equals(ContentWorld.PAGE)) { + StringBuilder sourceWrapped = new StringBuilder(); + if (!contentWorlds.contains(contentWorld)) { + contentWorlds.add(contentWorld); + + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + sourceWrapped.append(";").append(script.getSource()); + } + } + sourceWrapped.append(source); + return wrapSourceCodeInContentWorld(contentWorld, sourceWrapped.toString()); + } + return source; + } + + public String wrapSourceCodeInContentWorld(@Nullable ContentWorld contentWorld, String source) { + String sourceWrapped = contentWorld == null || contentWorld.equals(ContentWorld.PAGE) ? source : + CONTENT_WORLD_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME, escapeContentWorldName(contentWorld.getName())) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source)); + + return sourceWrapped; + } + + public static String escapeCode(String code) { + String escapedCode = JSONObject.quote(code); + // escapedCode = escapedCode.substring(1, escapedCode.length() - 1); + return escapedCode; + } + + public static String escapeContentWorldName(String name) { + return name.replaceAll("'", "\\\\'"); + } + + public LinkedHashSet getUserOnlyScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.userOnlyScripts.get(injectionTime)); + } + + public boolean addUserOnlyScript(UserScript userOnlyScript) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).add(userOnlyScript); + } + + public void addUserOnlyScripts(List userOnlyScripts) { + for (UserScript userOnlyScript : userOnlyScripts) { + this.addUserOnlyScript(userOnlyScript); + } + } + + public boolean removeUserOnlyScript(UserScript userOnlyScript) { + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).remove(userOnlyScript); + } + + public boolean removeUserOnlyScriptAt(int index, UserScriptInjectionTime injectionTime) { + UserScript userOnlyScript = new ArrayList<>(this.userOnlyScripts.get(injectionTime)).get(index); + return this.removeUserOnlyScript(userOnlyScript); + } + + public void removeAllUserOnlyScripts() { + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getPluginScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.pluginScripts.get(injectionTime)); + } + + public LinkedHashSet getPluginScriptsRequiredInAllContentWorlds() { + LinkedHashSet pluginScriptsRequired = new LinkedHashSet<>(); + LinkedHashSet scripts = this.getPluginScriptsAt(UserScriptInjectionTime.AT_DOCUMENT_START); + for (PluginScript script : scripts) { + if (script.isRequiredInAllContentWorlds()) { + pluginScriptsRequired.add(script); + } + } + return pluginScriptsRequired; + } + + public boolean addPluginScript(PluginScript pluginScript) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + return this.pluginScripts.get(pluginScript.getInjectionTime()).add(pluginScript); + } + + public void addPluginScripts(List pluginScripts) { + for (PluginScript pluginScript : pluginScripts) { + this.addPluginScript(pluginScript); + } + } + + public boolean removePluginScript(PluginScript pluginScript) { + return this.pluginScripts.get(pluginScript.getInjectionTime()).remove(pluginScript); + } + + public boolean removePluginScriptAt(int index, UserScriptInjectionTime injectionTime) { + PluginScript pluginScript = new ArrayList<>(this.pluginScripts.get(injectionTime)).get(index); + return this.removePluginScript(pluginScript); + } + + public void removeAllPluginScripts() { + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getUserOnlyScriptAsList() { + LinkedHashSet userOnlyScripts = new LinkedHashSet<>(); + Collection> collection = this.userOnlyScripts.values(); + for (LinkedHashSet list : collection) { + userOnlyScripts.addAll(list); + } + return userOnlyScripts; + } + + public LinkedHashSet getPluginScriptAsList() { + LinkedHashSet pluginScripts = new LinkedHashSet<>(); + Collection> collection = this.pluginScripts.values(); + for (LinkedHashSet list : collection) { + pluginScripts.addAll(list); + } + return pluginScripts; + } + + public void resetContentWorlds() { + this.contentWorlds.clear(); + this.contentWorlds.add(ContentWorld.PAGE); + + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + } + + public boolean containsPluginScript(PluginScript pluginScript) { + return this.getPluginScriptAsList().contains(pluginScript); + } + + public boolean containsPluginScriptByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + return true; + } + } + return false; + } + + public boolean containsUserOnlyScript(UserScript userOnlyScript) { + return this.getUserOnlyScriptAsList().contains(userOnlyScript); + } + + public boolean containsUserOnlyScriptByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + return true; + } + } + + return false; + } + + public void removePluginScriptsByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + this.removePluginScript(pluginScript); + } + } + } + + public void removeUserOnlyScriptsByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + this.removeUserOnlyScript(userOnlyScript); + } + } + } + + @NonNull + public LinkedHashSet getContentWorlds() { + return new LinkedHashSet<>(this.contentWorlds); + } + + private static final String USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String CONTENT_WORLDS_GENERATOR_JS_SOURCE = "(function() {" + + " var contentWorldNames = [" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY + "];" + + " for (var contentWorldName of contentWorldNames) {" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_' + contentWorldName;" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.innerHTML = "+ PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + " }" + + "})();"; + + private static final String CONTENT_WORLD_WRAPPER_JS_SOURCE = "(function() {" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME + "';" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.innerHTML = "+ PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + "})();"; + + private static final String DOCUMENT_READY_WRAPPER_JS_SOURCE = "if (document.readyState === 'interactive' || document.readyState === 'complete') { " + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "} else {" + + " document.addEventListener('DOMContentLoaded', function() {" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + " });" + + "}"; +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScript.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScript.java new file mode 100644 index 00000000..c4bdc991 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScript.java @@ -0,0 +1,105 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class UserScript { + @Nullable + private String groupName; + @NonNull + private String source; + @NonNull + private UserScriptInjectionTime injectionTime; + @NonNull + private ContentWorld contentWorld; + + public UserScript(@Nullable String groupName, @NonNull String source, @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld) { + this.groupName = groupName; + this.source = source; + this.injectionTime = injectionTime; + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + } + + @Nullable + public static UserScript fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String groupName = (String) map.get("groupName"); + String source = (String) map.get("source"); + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.fromValue((int) map.get("injectionTime")); + ContentWorld contentWorld = ContentWorld.fromMap((Map) map.get("contentWorld")); + assert source != null; + return new UserScript(groupName, source, injectionTime, contentWorld); + } + + @Nullable + public String getGroupName() { + return groupName; + } + + public void setGroupName(@Nullable String groupName) { + this.groupName = groupName; + } + + @NonNull + public String getSource() { + return source; + } + + public void setSource(@NonNull String source) { + this.source = source; + } + + @NonNull + public UserScriptInjectionTime getInjectionTime() { + return injectionTime; + } + + public void setInjectionTime(@NonNull UserScriptInjectionTime injectionTime) { + this.injectionTime = injectionTime; + } + + @NonNull + public ContentWorld getContentWorld() { + return contentWorld; + } + + public void setContentWorld(@Nullable ContentWorld contentWorld) { + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserScript that = (UserScript) o; + + if (groupName != null ? !groupName.equals(that.groupName) : that.groupName != null) return false; + if (!source.equals(that.source)) return false; + if (injectionTime != that.injectionTime) return false; + return contentWorld.equals(that.contentWorld); + } + + @Override + public int hashCode() { + int result = groupName != null ? groupName.hashCode() : 0; + result = 31 * result + source.hashCode(); + result = 31 * result + injectionTime.hashCode(); + result = 31 * result + contentWorld.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserScript{" + + "groupName='" + groupName + '\'' + + ", source='" + source + '\'' + + ", injectionTime=" + injectionTime + + ", contentWorld=" + contentWorld + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScriptInjectionTime.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScriptInjectionTime.java new file mode 100644 index 00000000..2ead9b8b --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/UserScriptInjectionTime.java @@ -0,0 +1,28 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +public enum UserScriptInjectionTime { + AT_DOCUMENT_START (0), + AT_DOCUMENT_END (1); + + private final int value; + + private UserScriptInjectionTime(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static UserScriptInjectionTime fromValue(int value) { + for( UserScriptInjectionTime type : UserScriptInjectionTime.values()) { + if(value == type.toValue()) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int toValue() { + return this.value; + } +} diff --git a/android/src/main/res/layout/activity_web_view.xml b/android/src/main/res/layout/activity_web_view.xml index cb162468..9246e66e 100755 --- a/android/src/main/res/layout/activity_web_view.xml +++ b/android/src/main/res/layout/activity_web_view.xml @@ -7,10 +7,10 @@ android:layout_height="match_parent" android:clickable="true" android:focusableInTouchMode="true" - tools:context=".InAppBrowser.InAppBrowserActivity" + tools:context=".in_app_browser.InAppBrowserActivity" android:focusable="true"> - diff --git a/android/src/main/res/menu/menu_main.xml b/android/src/main/res/menu/menu_main.xml index 317b27f2..acb3e94a 100755 --- a/android/src/main/res/menu/menu_main.xml +++ b/android/src/main/res/menu/menu_main.xml @@ -3,7 +3,7 @@ xmlns:appcompat="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - tools:context=".InAppBrowser.InAppBrowserActivity"> + tools:context=".in_app_browser.InAppBrowserActivity"> (); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -50,10 +52,207 @@ void main() { ), ); final InAppWebViewController controller = await controllerCompleter.future; - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://flutter.dev/'); }); + testWidgets('inappwebview set/get options', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + javaScriptEnabled: false + ) + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + InAppWebViewGroupOptions? options = await controller.getOptions(); + expect(options, isNotNull); + expect(options!.crossPlatform.javaScriptEnabled, false); + + await controller.setOptions(options: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + javaScriptEnabled: true + ) + )); + + options = await controller.getOptions(); + expect(options, isNotNull); + expect(options!.crossPlatform.javaScriptEnabled, true); + }); + + group('javascript code evaluation', () { + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('about:blank') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + var result = await controller.evaluateJavascript(source: """ + [1, true, ["bar", 5], {"foo": "baz"}]; + """); + expect(result, isNotNull); + expect(result[0], 1); + expect(result[1], true); + expect(listEquals(result[2] as List?, ["bar", 5]), true); + expect(mapEquals(result[3]?.cast(), {"foo": "baz"}), true); + }); + + testWidgets('evaluateJavascript with content world', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('about:blank') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await controller.evaluateJavascript(source: "var foo = 49;", contentWorld: ContentWorld.world(name: "custom-world")); + var result = await controller.evaluateJavascript(source: "foo"); + expect(result, isNull); + + result = await controller.evaluateJavascript(source: "foo", contentWorld: ContentWorld.world(name: "custom-world")); + expect(result, 49); + }); + + testWidgets('callAsyncJavaScript', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('about:blank') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + final String functionBody = """ + var p = new Promise(function (resolve, reject) { + window.setTimeout(function() { + if (x >= 0) { + resolve(x); + } else { + reject(y); + } + }, 1000); + }); + await p; + return p; + """; + + var result = await controller.callAsyncJavaScript(functionBody: functionBody, arguments: {'x': 49, 'y': 'error message'}); + expect(result, isNotNull); + expect(result!.error, isNull); + expect(result.value, 49); + + result = await controller.callAsyncJavaScript(functionBody: functionBody, arguments: {'x': -49, 'y': 'error message'}); + expect(result, isNotNull); + expect(result!.value, isNull); + expect(result.error, 'error message'); + }); + + testWidgets('callAsyncJavaScript with content world', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('about:blank') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await controller.callAsyncJavaScript(functionBody: "window.foo = 49;", contentWorld: ContentWorld.world(name: "custom-world")); + var result = await controller.callAsyncJavaScript(functionBody: "return window.foo;"); + expect(result, isNotNull); + expect(result!.error, isNull); + expect(result.value, isNull); + + result = await controller.callAsyncJavaScript(functionBody: "return window.foo;", contentWorld: ContentWorld.world(name: "custom-world")); + expect(result, isNotNull); + expect(result!.error, isNull); + expect(result.value, 49); + }); + }); + testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -62,7 +261,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -70,8 +271,8 @@ void main() { ), ); final InAppWebViewController controller = await controllerCompleter.future; - await controller.loadUrl(url: 'https://www.google.com/'); - final String? currentUrl = await controller.getUrl(); + await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse('https://www.google.com/'))); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://www.google.com/'); }); @@ -85,7 +286,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -95,10 +298,10 @@ void main() { ) ), onLoadStart: (controller, url) { - pageStarts.add(url!); + pageStarts.add(url!.toString()); }, onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -107,9 +310,11 @@ void main() { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl(url: 'https://flutter-header-echo.herokuapp.com/', - headers: headers); - final String? currentUrl = await controller.getUrl(); + await controller.loadUrl(urlRequest: URLRequest( + url: Uri.parse('https://flutter-header-echo.herokuapp.com/'), + headers: headers + )); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); await pageStarts.stream.firstWhere((String url) => url == currentUrl); @@ -165,7 +370,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: Uri.encodeFull('file://${fileHtml.path}'), + initialUrlRequest: URLRequest( + url: Uri.parse('file://${fileHtml.path}') + ), onConsoleMessage: (controller, consoleMessage) { consoleMessageShouldNotComplete.complete(consoleMessage); }, @@ -182,10 +389,12 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: Uri.encodeFull('file://${fileHtml.path}'), + initialUrlRequest: URLRequest( + url: Uri.parse('file://${fileHtml.path}') + ), initialOptions: InAppWebViewGroupOptions( ios: IOSInAppWebViewOptions( - allowingReadAccessTo: Uri.encodeFull('file://${appSupportDir.path}/') + allowingReadAccessTo: Uri.parse('file://${appSupportDir.path}/') ) ), onConsoleMessage: (controller, consoleMessage) { @@ -207,7 +416,9 @@ void main() { child: InAppWebView( key: GlobalKey(), onWebViewCreated: (controller) { - controller.loadUrl(url: Uri.encodeFull('file://${fileHtml.path}')); + controller.loadUrl(urlRequest: URLRequest( + url: Uri.parse('file://${fileHtml.path}') + )); }, onConsoleMessage: (controller, consoleMessage) { consoleMessageShouldNotComplete.complete(consoleMessage); @@ -226,8 +437,9 @@ void main() { child: InAppWebView( key: GlobalKey(), onWebViewCreated: (controller) { - controller.loadUrl(url: Uri.encodeFull('file://${fileHtml.path}'), - iosAllowingReadAccessTo: Uri.encodeFull('file://${appSupportDir.path}/')); + controller.loadUrl(urlRequest: URLRequest( + url: Uri.parse('file://${fileHtml.path}')), + iosAllowingReadAccessTo: Uri.parse('file://${appSupportDir.path}/')); }, onConsoleMessage: (controller, consoleMessage) { consoleMessageCompleter.complete(consoleMessage); @@ -287,7 +499,7 @@ void main() { ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; await handlerFoo.future; @@ -328,7 +540,9 @@ void main() { final InAppWebView webView = InAppWebView( key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$resizeTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); @@ -395,7 +609,9 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: InAppWebView( - initialUrl: 'about:blank', + initialUrlRequest: URLRequest( + url: Uri.parse('about:blank') + ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( javaScriptEnabled: true, @@ -441,9 +657,14 @@ void main() { var video = document.getElementById("video"); return video.paused; } - function isFullScreen() { - var video = document.getElementById("video"); - return video.webkitDisplayingFullscreen; + function exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } } @@ -467,7 +688,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -498,7 +721,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -527,13 +752,16 @@ void main() { Completer controllerCompleter = Completer(); Completer pageLoaded = Completer(); + Completer onEnterFullscreenCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -549,29 +777,31 @@ void main() { onLoadStop: (controller, url) { pageLoaded.complete(); }, + onEnterFullscreen: (controller) { + onEnterFullscreenCompleter.complete(); + }, ), ), ); - InAppWebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - bool isFullScreen = - await controller.evaluateJavascript(source: 'isFullScreen();'); - expect(isFullScreen, false); + await pageLoaded.future; + expect(onEnterFullscreenCompleter.future, doesNotComplete); }); testWidgets('Video plays fullscreen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); + Completer controllerCompleter = Completer(); Completer pageLoaded = Completer(); + Completer onEnterFullscreenCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -587,6 +817,49 @@ void main() { onLoadStop: (controller, url) { pageLoaded.complete(); }, + onEnterFullscreen: (controller) { + onEnterFullscreenCompleter.complete(); + }, + ), + ), + ); + + await pageLoaded.future; + await expectLater(onEnterFullscreenCompleter.future, completes); + }, skip: true); + + testWidgets('exit fullscreen event', + (WidgetTester tester) async { + Completer controllerCompleter = Completer(); + Completer pageLoaded = Completer(); + Completer onExitFullscreenCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + javaScriptEnabled: true, + mediaPlaybackRequiresUserGesture: false, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: false + ) + ), + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + onExitFullscreen: (controller) { + onExitFullscreenCompleter.complete(); + }, ), ), ); @@ -594,11 +867,14 @@ void main() { InAppWebViewController controller = await controllerCompleter.future; await pageLoaded.future; - bool isFullScreen = await controller.evaluateJavascript(source: 'isFullScreen();'); - expect(isFullScreen, true); - }, skip: true /*https://github.com/flutter/flutter/issues/72572 */); + await Future.delayed(Duration(seconds: 2)); + await controller.evaluateJavascript(source: "exitFullscreen();"); + + await expectLater(onExitFullscreenCompleter.future, completes); + }, skip: true /*!Platform.isAndroid*/); }); + group('Audio playback policy', () { String audioTestBase64 = ""; setUpAll(() async { @@ -641,7 +917,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -677,7 +955,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -726,7 +1006,9 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: InAppWebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -782,8 +1064,9 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: InAppWebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$scrollTestPageBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -855,8 +1138,9 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: InAppWebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$scrollTestPageBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -913,7 +1197,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: pageEncoded, + initialUrlRequest: URLRequest( + url: Uri.parse(pageEncoded) + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -923,13 +1209,13 @@ void main() { useShouldOverrideUrlLoading: true ), ), - shouldOverrideUrlLoading: (controller, shouldOverrideUrlLoadingRequest) async { - return (shouldOverrideUrlLoadingRequest.url.contains('youtube.com')) - ? ShouldOverrideUrlLoadingAction.CANCEL - : ShouldOverrideUrlLoadingAction.ALLOW; + shouldOverrideUrlLoading: (controller, navigationAction) async { + return (navigationAction.request.url!.host.contains('youtube.com')) + ? NavigationActionPolicy.CANCEL + : NavigationActionPolicy.ALLOW; }, onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -941,7 +1227,7 @@ void main() { .evaluateJavascript(source: 'location.href = "https://www.google.com/"'); await pageLoads.stream.first; // Wait for the next page load. - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://www.google.com/'); }); @@ -954,7 +1240,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: pageEncoded, + initialUrlRequest: URLRequest( + url: Uri.parse(pageEncoded) + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -964,13 +1252,14 @@ void main() { useShouldOverrideUrlLoading: true ), ), - shouldOverrideUrlLoading: (controller, shouldOverrideUrlLoadingRequest) async { - return (shouldOverrideUrlLoadingRequest.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) - ? ShouldOverrideUrlLoadingAction.ALLOW - : ShouldOverrideUrlLoadingAction.CANCEL; + shouldOverrideUrlLoading: (controller, navigationAction) async { + var isFirstLoad = navigationAction.request.url!.scheme == "data"; + return (isFirstLoad || navigationAction.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) + ? NavigationActionPolicy.ALLOW + : NavigationActionPolicy.CANCEL; }, onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -985,12 +1274,12 @@ void main() { // to give the test a chance to fail. await pageLoads.stream.map((event) => event as String?).first .timeout(const Duration(milliseconds: 500), onTimeout: () => null); - String? currentUrl = await controller.getUrl(); + String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, isNot('https://www.google.com/')); await controller.evaluateJavascript(source: 'document.querySelector("#link").click();'); await pageLoads.stream.first; // Wait for the next page load. - currentUrl = await controller.getUrl(); + currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://github.com/pichillilorenzo/flutter_inappwebview'); }, skip: !Platform.isIOS); @@ -1004,7 +1293,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: pageEncoded, + initialUrlRequest: URLRequest( + url: Uri.parse(pageEncoded) + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1014,13 +1305,13 @@ void main() { useShouldOverrideUrlLoading: true ), ), - shouldOverrideUrlLoading: (controller, shouldOverrideUrlLoadingRequest) async { - return (shouldOverrideUrlLoadingRequest.url.contains('youtube.com')) - ? ShouldOverrideUrlLoadingAction.CANCEL - : ShouldOverrideUrlLoadingAction.ALLOW; + shouldOverrideUrlLoading: (controller, navigationAction) async { + return (navigationAction.request.url!.host.contains('youtube.com')) + ? NavigationActionPolicy.CANCEL + : NavigationActionPolicy.ALLOW; }, onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -1036,7 +1327,7 @@ void main() { // to give the test a chance to fail. await pageLoads.stream.map((event) => event as String?).first .timeout(const Duration(milliseconds: 500), onTimeout: () => null); - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, isNot(contains('youtube.com'))); }); @@ -1050,7 +1341,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: pageEncoded, + initialUrlRequest: URLRequest( + url: Uri.parse(pageEncoded) + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1060,15 +1353,15 @@ void main() { useShouldOverrideUrlLoading: true ), ), - shouldOverrideUrlLoading: (controller, shouldOverrideUrlLoadingRequest) async { - var action = ShouldOverrideUrlLoadingAction.CANCEL; - action = await Future.delayed( + shouldOverrideUrlLoading: (controller, navigationAction) async { + var action = NavigationActionPolicy.CANCEL; + action = await Future.delayed( const Duration(milliseconds: 10), - () => ShouldOverrideUrlLoadingAction.ALLOW); + () => NavigationActionPolicy.ALLOW); return action; }, onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -1080,7 +1373,7 @@ void main() { .evaluateJavascript(source: 'location.href = "https://www.google.com"'); await pageLoads.stream.first; // Wait for second page to load. - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'https://www.google.com/'); }); }); @@ -1088,18 +1381,18 @@ void main() { testWidgets('onLoadError', (WidgetTester tester) async { final Completer errorUrlCompleter = Completer(); final Completer errorCodeCompleter = Completer(); - final Completer errorMessageCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', + initialUrlRequest: URLRequest( + url: Uri.parse('https://www.notawebsite..com') + ), onLoadError: (controller, url, code, message) { - errorUrlCompleter.complete(url); + errorUrlCompleter.complete(url.toString()); errorCodeCompleter.complete(code); - errorMessageCompleter.complete(message); }, ), ), @@ -1107,7 +1400,6 @@ void main() { final String url = await errorUrlCompleter.future; final int code = await errorCodeCompleter.future; - final String message = await errorMessageCompleter.future; if (Platform.isAndroid) { expect(code, -2); @@ -1128,10 +1420,11 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+') + ), onLoadError: (controller, url, code, message) { - errorUrlCompleter.complete(url); + errorUrlCompleter.complete(url.toString()); errorCodeCompleter.complete(code); errorMessageCompleter.complete(message); }, @@ -1156,7 +1449,9 @@ void main() { height: 300, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), initialOptions: InAppWebViewGroupOptions( ios: IOSInAppWebViewOptions( allowsBackForwardNavigationGestures: true @@ -1170,7 +1465,7 @@ void main() { ), ); final InAppWebViewController controller = await controllerCompleter.future; - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, contains('flutter.dev')); }); @@ -1194,7 +1489,7 @@ void main() { ), ), onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, ), ), @@ -1204,7 +1499,7 @@ void main() { await controller.evaluateJavascript(source: 'window.open("about:blank", "_blank");'); await pageLoads.stream.first; - final String? currentUrl = await controller.getUrl(); + final String? currentUrl = (await controller.getUrl())?.toString(); expect(currentUrl, 'about:blank'); }); @@ -1219,6 +1514,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1229,9 +1527,8 @@ void main() { ), ), onLoadStop: (controller, url) { - pageLoads.add(url!); + pageLoads.add(url!.toString()); }, - initialUrl: 'https://flutter.dev', ), ), ); @@ -1241,11 +1538,11 @@ void main() { await controller .evaluateJavascript(source: 'window.open("https://github.com/flutter");'); await pageLoads.stream.first; - expect(await controller.getUrl(), contains('github.com/flutter')); + expect((await controller.getUrl())?.toString(), contains('github.com/flutter')); await controller.goBack(); await pageLoads.stream.first; - expect(await controller.getUrl(), contains('flutter.dev')); + expect((await controller.getUrl())?.toString(), contains('flutter.dev')); }, skip: !Platform.isAndroid, ); @@ -1292,6 +1589,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('data:text/html;charset=utf-8;base64,$openWindowTestBase64') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1301,8 +1601,6 @@ void main() { javaScriptCanOpenWindowsAutomatically: true ), ), - initialUrl: - 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', onLoadStop: (controller, url) { pageLoadCompleter.complete(); }, @@ -1359,7 +1657,6 @@ void main() { """), - initialHeaders: {}, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( clearCache: true, @@ -1370,7 +1667,7 @@ void main() { controllerCompleter.complete(controller); }, shouldInterceptAjaxRequest: (controller, ajaxRequest) async { - if (ajaxRequest.url!.endsWith("/test-ajax-post")) { + if (ajaxRequest.url!.toString().endsWith("/test-ajax-post")) { ajaxRequest.responseType = 'json'; ajaxRequest.data = "firstname=Foo2&lastname=Bar2"; shouldInterceptAjaxPostRequestCompleter.complete(controller); @@ -1378,14 +1675,14 @@ void main() { return ajaxRequest; }, onAjaxReadyStateChange: (controller, ajaxRequest) async { - if (ajaxRequest.readyState == AjaxRequestReadyState.DONE && ajaxRequest.status == 200 && ajaxRequest.url!.endsWith("/test-ajax-post")) { + if (ajaxRequest.readyState == AjaxRequestReadyState.DONE && ajaxRequest.status == 200 && ajaxRequest.url!.toString().endsWith("/test-ajax-post")) { Map res = ajaxRequest.response; onAjaxReadyStateChangeCompleter.complete(res); } return AjaxRequestAction.PROCEED; }, onAjaxProgress: (controller, ajaxRequest) async { - if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD && ajaxRequest.url!.endsWith("/test-ajax-post")) { + if (ajaxRequest.event!.type == AjaxRequestEventType.LOAD && ajaxRequest.url!.toString().endsWith("/test-ajax-post")) { Map res = ajaxRequest.response; onAjaxProgressCompleter.complete(res); } @@ -1395,7 +1692,6 @@ void main() { ), ); - final InAppWebViewController controller = await controllerCompleter.future; await shouldInterceptAjaxPostRequestCompleter.future; final Map onAjaxReadyStateChangeValue = await onAjaxReadyStateChangeCompleter.future; final Map onAjaxProgressValue = await onAjaxProgressCompleter.future; @@ -1412,7 +1708,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1441,7 +1739,6 @@ void main() { ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; await expectLater(pageLoaded.future, completes); }); @@ -1454,7 +1751,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1464,13 +1763,13 @@ void main() { ) ), onLoadStop: (controller, url) { - pageLoaded.complete(url); + pageLoaded.complete(url!.toString()); }, ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; - final String url = await pageLoaded.future; + + final url = Uri.parse(await pageLoaded.future); await cookieManager.setCookie(url: url, name: "myCookie", value: "myValue"); List cookies = await cookieManager.getCookies(url: url); @@ -1540,7 +1839,6 @@ void main() { """), - initialHeaders: {}, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( clearCache: true, @@ -1559,7 +1857,7 @@ void main() { }); }, shouldInterceptFetchRequest: (controller, fetchRequest) async { - if (fetchRequest.url!.endsWith("/test-ajax-post")) { + if (fetchRequest.url!.toString().endsWith("/test-ajax-post")) { fetchRequest.body = utf8.encode("""{ "firstname": "Foo2", "lastname": "Bar2" @@ -1573,8 +1871,6 @@ void main() { ), ); - final InAppWebViewController controller = await controllerCompleter.future; - var fetchGetCompleterValue = await fetchGetCompleter.future; expect(fetchGetCompleterValue, '200'); @@ -1590,8 +1886,8 @@ void main() { final Completer pageLoaded = Completer(); httpAuthCredentialDatabase.setHttpAuthCredential( - protectionSpace: ProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081), - credential: HttpAuthCredential(username: "USERNAME", password: "PASSWORD") + protectionSpace: URLProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081), + credential: URLCredential(username: "USERNAME", password: "PASSWORD") ); await tester.pumpWidget( @@ -1599,7 +1895,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: "http://${environment["NODE_SERVER_IP"]}:8081/", + initialUrlRequest: URLRequest( + url: Uri.parse("http://${environment["NODE_SERVER_IP"]}:8081/") + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1624,13 +1922,13 @@ void main() { expect(h1Content, "Authorized"); var credentials = await httpAuthCredentialDatabase.getHttpAuthCredentials(protectionSpace: - ProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081) + URLProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081) ); expect(credentials.length, 1); await httpAuthCredentialDatabase.clearAllAuthCredentials(); credentials = await httpAuthCredentialDatabase.getHttpAuthCredentials(protectionSpace: - ProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081) + URLProtectionSpace(host: environment["NODE_SERVER_IP"]!, protocol: "http", realm: "Node", port: 8081) ); expect(credentials, isEmpty); }); @@ -1644,7 +1942,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: "http://${environment["NODE_SERVER_IP"]}:8081/", + initialUrlRequest: URLRequest( + url: Uri.parse("http://${environment["NODE_SERVER_IP"]}:8081/") + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -1691,7 +1991,7 @@ void main() { ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; + final ConsoleMessage consoleMessage = await onConsoleMessageCompleter.future; expect(consoleMessage.message, 'message'); expect(consoleMessage.messageLevel, ConsoleMessageLevel.LOG); @@ -1717,27 +2017,24 @@ void main() { controllerCompleter.complete(controller); }, onLoadStop: (controller, url) { - if (url == "https://flutter.dev/") { + if (url!.toString() == "https://flutter.dev/") { pageLoaded.complete(); } }, - onCreateWindow: (controller, createWindowRequest) async { - controller.loadUrl(url: createWindowRequest.url!); + onCreateWindow: (controller, createNavigationAction) async { + controller.loadUrl(urlRequest: createNavigationAction.request); return false; }, ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; + await expectLater(pageLoaded.future, completes); }); testWidgets('onCreateWindow return true', (WidgetTester tester) async { - int? windowId; - String? windowUrl; - final Completer controllerCompleter = Completer(); - final Completer onCreateWindowCompleter = Completer(); + final Completer onCreateWindowCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -1748,23 +2045,23 @@ void main() { crossPlatform: InAppWebViewOptions( clearCache: true, javaScriptCanOpenWindowsAutomatically: true, + ), + android: AndroidInAppWebViewOptions( + supportMultipleWindows: true ) ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, - onCreateWindow: (controller, createWindowRequest) async { - windowId = createWindowRequest.windowId; - windowUrl = createWindowRequest.url; - onCreateWindowCompleter.complete(); + onCreateWindow: (controller, createNavigationAction) async { + onCreateWindowCompleter.complete(createNavigationAction.windowId); return true; }, ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; - await expectLater(onCreateWindowCompleter.future, completes); + var windowId = await onCreateWindowCompleter.future; final Completer windowControllerCompleter = Completer(); final Completer windowPageLoaded = Completer(); @@ -1775,8 +2072,7 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialFile: windowUrl!, - windowId: windowId!, + windowId: windowId, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( clearCache: true, @@ -1786,7 +2082,7 @@ void main() { windowControllerCompleter.complete(controller); }, onLoadStop: (controller, url) async { - windowPageLoaded.complete(url!); + windowPageLoaded.complete(url!.toString()); await controller.evaluateJavascript(source: "window.close();"); }, onCloseWindow: (controller) { @@ -1841,12 +2137,12 @@ void main() { controllerCompleter.complete(controller); }, onDownloadStart: (controller, url) { - onDownloadStartCompleter.complete(url); + onDownloadStartCompleter.complete(url.toString()); }, ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; + final String url = await onDownloadStartCompleter.future; expect(url, "http://${environment["NODE_SERVER_IP"]}:8082/test-download-file"); }); @@ -1879,7 +2175,7 @@ void main() { ), ), ); - final InAppWebViewController controller = await controllerCompleter.future; + final int numberOfMatches = await numberOfMatchesCompleter.future; expect(numberOfMatches, 2); }); @@ -1939,7 +2235,6 @@ void main() { ), ); - final InAppWebViewController controller = await controllerCompleter.future; await pageLoaded.future; final JsAlertRequest jsAlertRequest = await alertCompleter.future; @@ -1961,9 +2256,11 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://google.com/404', + initialUrlRequest: URLRequest( + url: Uri.parse('https://google.com/404') + ), onLoadHttpError: (controller, url, statusCode, description) async { - errorUrlCompleter.complete(url); + errorUrlCompleter.complete(url.toString()); statusCodeCompleter.complete(statusCode); }, ), @@ -2000,10 +2297,10 @@ void main() { imageLoaded.complete(); }); }, - onLoadResourceCustomScheme: (controller, scheme, url) async { - if (scheme == "my-special-custom-scheme") { - var bytes = await rootBundle.load("test_assets/" + url.replaceFirst("my-special-custom-scheme://", "", 0)); - var response = CustomSchemeResponse(data: bytes.buffer.asUint8List(), contentType: "image/svg+xml", contentEnconding: "utf-8"); + onLoadResourceCustomScheme: (controller, url) async { + if (url.scheme == "my-special-custom-scheme") { + var bytes = await rootBundle.load("test_assets/" + url.toString().replaceFirst("my-special-custom-scheme://", "", 0)); + var response = CustomSchemeResponse(data: bytes.buffer.asUint8List(), contentType: "image/svg+xml", contentEncoding: "utf-8"); return response; } return null; @@ -2012,7 +2309,6 @@ void main() { ), ); - final InAppWebViewController controller = await controllerCompleter.future; await expectLater(imageLoaded.future, completes); }); @@ -2043,7 +2339,7 @@ void main() { pageLoaded.complete(); }, onLoadResource: (controller, response) async { - resourceLoaded.add(response.url!); + resourceLoaded.add(response.url!.toString()); if (resourceLoaded.length == resourceList.length) { loadedResourceCompleter.complete(); } @@ -2069,7 +2365,9 @@ void main() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: "https://flutter.dev/", + initialUrlRequest: URLRequest( + url: Uri.parse("https://flutter.dev/") + ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( clearCache: true @@ -2082,9 +2380,9 @@ void main() { pageLoaded.complete(); }, onUpdateVisitedHistory: (controller, url, androidIsReload) async { - if (url!.endsWith("second-push")) { + if (url!.toString().endsWith("second-push")) { secondPushCompleter.complete(); - } else if (url.endsWith("first-push")) { + } else if (url.toString().endsWith("first-push")) { firstPushCompleter.complete(); } }, @@ -2108,10 +2406,10 @@ setTimeout(function() { """); await firstPushCompleter.future; - expect(await controller.getUrl(), 'https://flutter.dev/first-push'); + expect((await controller.getUrl())?.toString(), 'https://flutter.dev/first-push'); await secondPushCompleter.future; - expect(await controller.getUrl(), 'https://flutter.dev/second-push'); + expect((await controller.getUrl())?.toString(), 'https://flutter.dev/second-push'); }); testWidgets('onProgressChanged', (WidgetTester tester) async { @@ -2121,7 +2419,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( clearCache: true, @@ -2145,7 +2445,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'chrome://safe-browsing/match?type=malware', + initialUrlRequest: URLRequest( + url: Uri.parse('chrome://safe-browsing/match?type=malware') + ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( // if I set javaScriptEnabled to true, it will crash! @@ -2160,7 +2462,7 @@ setTimeout(function() { controller.android.startSafeBrowsing(); }, onLoadStop: (controller, url) { - pageLoaded.complete(url); + pageLoaded.complete(url!.toString()); }, androidOnSafeBrowsingHit: (controller, url, threatType) async { return SafeBrowsingResponse(report: true, action: SafeBrowsingResponseAction.PROCEED); @@ -2182,7 +2484,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -2215,7 +2519,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: "https://${environment["NODE_SERVER_IP"]}:4433/", + initialUrlRequest: URLRequest( + url: Uri.parse("https://${environment["NODE_SERVER_IP"]}:4433/") + ), onWebViewCreated: (controller) { controllerCompleter.complete(controller); }, @@ -2249,12 +2555,14 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onLoadStop: (controller, url) async { await controller.evaluateJavascript(source: "window.print();"); }, onPrint: (controller, url) { - onPrintCompleter.complete(url); + onPrintCompleter.complete(url?.toString()); }, ), ), @@ -2270,7 +2578,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onLoadStop: (controller, url) async { await controller.evaluateJavascript(source: 'window.dispatchEvent(new Event("focus"));'); }, @@ -2290,7 +2600,9 @@ setTimeout(function() { textDirection: TextDirection.ltr, child: InAppWebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), onLoadStop: (controller, url) async { await controller.evaluateJavascript(source: 'window.dispatchEvent(new Event("blur"));'); }, @@ -2302,4 +2614,390 @@ setTimeout(function() { ); await expectLater(onWindowBlurCompleter.future, completes); }); + + testWidgets('onPageCommitVisible', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer onPageCommitVisibleCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onPageCommitVisible: (controller, url) { + onPageCommitVisibleCompleter.complete(url?.toString()); + }, + ), + ), + ); + + final String? url = await onPageCommitVisibleCompleter.future; + expect(url, 'https://flutter.dev/'); + }); + + testWidgets('onTitleChanged', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onTitleChangedCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + onTitleChanged: (controller, title) { + if (title == "title test") { + onTitleChangedCompleter.complete(); + } + }, + ), + ), + ); + + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + await controller.evaluateJavascript(source: "document.title = 'title test';"); + await expectLater(onTitleChangedCompleter.future, completes); + }); + + testWidgets('androidOnPermissionRequest', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer> onPermissionRequestCompleter = Completer>(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://permission.site/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + androidOnPermissionRequest: (controller, origin, resources) async { + onPermissionRequestCompleter.complete(resources); + }, + ), + ), + ); + + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + await controller.evaluateJavascript(source: "document.querySelector('#camera').click();"); + final List resources = await onPermissionRequestCompleter.future; + + expect(listEquals(resources, ['android.webkit.resource.VIDEO_CAPTURE']), true); + }, skip: !Platform.isAndroid); + + testWidgets('androidShouldInterceptRequest', (WidgetTester tester) async { + List resourceList = [ + "https://getbootstrap.com/docs/4.3/dist/css/bootstrap.min.css", + "https://code.jquery.com/jquery-3.3.1.min.js", + "https://via.placeholder.com/100x50" + ]; + List resourceLoaded = []; + + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer loadedResourceCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialData: InAppWebViewInitialData(data: """ + + + + + + + + + + + placeholder 100x50 + + + """), + initialOptions: InAppWebViewGroupOptions( + android: AndroidInAppWebViewOptions( + useShouldInterceptRequest: true + ) + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + androidShouldInterceptRequest: (controller, request) async { + resourceLoaded.add(request.url.toString()); + if (resourceLoaded.length == resourceList.length) { + loadedResourceCompleter.complete(); + } + return null; + }, + ), + ), + ); + + await pageLoaded.future; + await loadedResourceCompleter.future; + expect(resourceLoaded, containsAll(resourceList)); + }, skip: !Platform.isAndroid); + + testWidgets('androidOnScaleChanged', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onScaleChangedCompleter = Completer(); + + var listenForScaleChange = false; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + androidOnScaleChanged: (controller, oldScale, newScale) { + if (listenForScaleChange) { + onScaleChangedCompleter.complete(); + } + }, + ), + ), + ); + + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + listenForScaleChange = true; + + await controller.evaluateJavascript(source: """ + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width, initial-scale=2.0, maximum-scale=2.0, minimum-scale=2.0, user-scalable=no'); + document.getElementsByTagName('head')[0].appendChild(meta); + """); + + await expectLater(onScaleChangedCompleter.future, completes); + }, skip: !Platform.isAndroid); + + testWidgets('androidOnReceivedIcon', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onReceivedIconCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + androidOnReceivedIcon: (controller, icon) { + onReceivedIconCompleter.complete(icon); + }, + ), + ), + ); + + await pageLoaded.future; + final Uint8List icon = await onReceivedIconCompleter.future; + expect(icon, isNotNull); + }, skip: !Platform.isAndroid); + + testWidgets('androidOnReceivedTouchIconUrl', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer onReceivedTouchIconUrlCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialData: InAppWebViewInitialData(data: """ + + + + + + + + + + + """), + initialOptions: InAppWebViewGroupOptions( + android: AndroidInAppWebViewOptions( + useShouldInterceptRequest: true + ) + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + androidOnReceivedTouchIconUrl: (controller, url, precomposed) { + onReceivedTouchIconUrlCompleter.complete(url.toString()); + }, + ), + ), + ); + + final String url = await onReceivedTouchIconUrlCompleter.future; + + expect(url, "https://placehold.it/72x72"); + }, skip: !Platform.isAndroid); + + testWidgets('androidOnJsBeforeUnload', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onJsBeforeUnloadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) async { + await controller.evaluateJavascript(source: """ + window.addEventListener('beforeunload', function (e) { + e.preventDefault(); + e.returnValue = ''; + }); + """); + if (!pageLoaded.isCompleted) { + pageLoaded.complete(); + } + }, + androidOnJsBeforeUnload: (controller, jsBeforeUnloadRequest) async { + onJsBeforeUnloadCompleter.complete(jsBeforeUnloadRequest.url.toString()); + }, + ), + ), + ); + + final InAppWebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + await controller.evaluateJavascript(source: "window.location.href = 'https://github.com/flutter';"); + final String url = await onJsBeforeUnloadCompleter.future; + expect(url, 'https://github.com/flutter'); + }, skip: true /*!Platform.isAndroid*/); + + group("iosOnNavigationResponse", () { + testWidgets('allow navigation', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onNavigationResponseCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + initialOptions: InAppWebViewGroupOptions( + ios: IOSInAppWebViewOptions( + useOnNavigationResponse: true + ) + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + iosOnNavigationResponse: (controller, navigationResponse) async { + onNavigationResponseCompleter.complete(navigationResponse.response!.url.toString()); + return IOSNavigationResponseAction.ALLOW; + }, + ), + ), + ); + + await pageLoaded.future; + final String url = await onNavigationResponseCompleter.future; + expect(url, 'https://flutter.dev/'); + }); + + testWidgets('cancel navigation', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer onNavigationResponseCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialUrlRequest: URLRequest( + url: Uri.parse('https://flutter.dev/') + ), + initialOptions: InAppWebViewGroupOptions( + ios: IOSInAppWebViewOptions( + useOnNavigationResponse: true + ) + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + iosOnNavigationResponse: (controller, navigationResponse) async { + onNavigationResponseCompleter.complete(navigationResponse.response!.url.toString()); + return IOSNavigationResponseAction.CANCEL; + }, + ), + ), + ); + + final String url = await onNavigationResponseCompleter.future; + expect(url, 'https://flutter.dev/'); + expect(pageLoaded.future, doesNotComplete); + }); + }, skip: !Platform.isIOS); } diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index ff2151af..b59a9d8e 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -2,12 +2,12 @@ # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/Users/lorenzopichilli/flutter" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" -export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" +export "FLUTTER_TARGET=integration_test/webview_flutter_test.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" +export "DART_DEFINES=RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d3fe73ee..6a66e670 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -257,6 +257,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", "${BUILT_PRODUCTS_DIR}/device_info/device_info.framework", "${BUILT_PRODUCTS_DIR}/flutter_downloader/flutter_downloader.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview/flutter_inappwebview.framework", @@ -266,6 +267,7 @@ ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_downloader.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview.framework", diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index d3674e9a..b224a399 100755 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -3,6 +3,7 @@ import Flutter //import flutter_downloader @UIApplicationMain + @objc class AppDelegate: FlutterAppDelegate { override func application( diff --git a/example/lib/chrome_safari_browser_example.screen.dart b/example/lib/chrome_safari_browser_example.screen.dart index 8bae4cc7..2f3a9a95 100755 --- a/example/lib/chrome_safari_browser_example.screen.dart +++ b/example/lib/chrome_safari_browser_example.screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'main.dart'; class MyChromeSafariBrowser extends ChromeSafariBrowser { - MyChromeSafariBrowser({browserFallback}) : super(bFallback: browserFallback); @override void onOpened() { @@ -24,7 +23,7 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { class ChromeSafariBrowserExampleScreen extends StatefulWidget { final ChromeSafariBrowser browser = - MyChromeSafariBrowser(browserFallback: InAppBrowser()); + MyChromeSafariBrowser(); @override _ChromeSafariBrowserExampleScreenState createState() => @@ -57,10 +56,10 @@ class _ChromeSafariBrowserExampleScreenState )), drawer: myDrawer(context: context), body: Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () async { await widget.browser.open( - url: "https://flutter.dev/", + url: Uri.parse("https://flutter.dev/"), options: ChromeSafariBrowserClassOptions( android: AndroidChromeCustomTabsOptions(addDefaultShareMenuItem: false, keepAliveEnabled: true), ios: IOSSafariOptions( diff --git a/example/lib/headless_in_app_webview.screen.dart b/example/lib/headless_in_app_webview.screen.dart index 00d39b9c..4109f9cc 100755 --- a/example/lib/headless_in_app_webview.screen.dart +++ b/example/lib/headless_in_app_webview.screen.dart @@ -19,7 +19,9 @@ class _HeadlessInAppWebViewExampleScreenState extends State 50) ? url.substring(0, 50) + "..." : url}"), ), Center( - child: RaisedButton( + child: ElevatedButton( onPressed: () async { await headlessWebView?.dispose(); await headlessWebView?.run(); @@ -82,7 +84,7 @@ class _HeadlessInAppWebViewExampleScreenState extends State shouldOverrideUrlLoading( - shouldOverrideUrlLoadingRequest) async { - print("\n\nOverride ${shouldOverrideUrlLoadingRequest.url}\n\n"); - return ShouldOverrideUrlLoadingAction.ALLOW; + Future shouldOverrideUrlLoading( + navigationAction) async { + print("\n\nOverride ${navigationAction.request.url}\n\n"); + return NavigationActionPolicy.ALLOW; } @override @@ -54,7 +54,7 @@ class MyInAppBrowser extends InAppBrowser { "ms ---> duration: " + response.duration.toString() + "ms " + - response.url!); + (response.url ?? '').toString()); } @override @@ -62,7 +62,7 @@ class MyInAppBrowser extends InAppBrowser { print(""" console output: message: ${consoleMessage.message} - messageLevel: ${consoleMessage.messageLevel!.toValue()} + messageLevel: ${consoleMessage.messageLevel.toValue()} """); } } @@ -93,23 +93,24 @@ class _InAppBrowserExampleScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( + ElevatedButton( onPressed: () async { - await widget.browser.openFile( - assetFilePath: "assets/index.html", + await widget.browser.openUrlRequest( + urlRequest: URLRequest(url: Uri.parse("https://flutter.dev")), options: InAppBrowserClassOptions( - inAppWebViewGroupOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: true, - useOnLoadResource: true, - )))); + inAppWebViewGroupOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: true, + useOnLoadResource: true, + ) + ))); }, child: Text("Open In-App Browser")), Container(height: 40), - RaisedButton( + ElevatedButton( onPressed: () async { await InAppBrowser.openWithSystemBrowser( - url: "https://flutter.dev/"); + url: Uri.parse("https://flutter.dev/")); }, child: Text("Open System Browser")), ]))); diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index c038e500..f20d3346 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -5,7 +5,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:path_provider/path_provider.dart'; // import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -61,6 +60,20 @@ class _InAppWebViewExampleScreenState extends State { super.dispose(); } + var options = InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: false, + mediaPlaybackRequiresUserGesture: false, + ), + android: AndroidInAppWebViewOptions( + useHybridComposition: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + // limitsNavigationsToAppBoundDomains: true // adds Service Worker API on iOS 14.0+ + ) + ); + @override Widget build(BuildContext context) { return Scaffold( @@ -88,26 +101,14 @@ class _InAppWebViewExampleScreenState extends State { child: InAppWebView( key: webViewKey, // contextMenu: contextMenu, - initialUrl: "https://flutter.dev", + initialUrlRequest: URLRequest( + url: Uri.parse("https://github.com") + ), // initialFile: "assets/index.html", - initialHeaders: {}, initialUserScripts: UnmodifiableListView([ ]), - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - useShouldOverrideUrlLoading: false, - mediaPlaybackRequiresUserGesture: false, - clearCache: true, - ), - android: AndroidInAppWebViewOptions( - useHybridComposition: false, - ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - // limitsNavigationsToAppBoundDomains: true // adds Service Worker API on iOS 14.0+ - ) - ), + initialOptions: options, onWebViewCreated: (controller) { webView = controller; print("onWebViewCreated"); @@ -115,15 +116,14 @@ class _InAppWebViewExampleScreenState extends State { onLoadStart: (controller, url) { print("onLoadStart $url"); setState(() { - this.url = url ?? ''; + this.url = url.toString(); }); }, androidOnPermissionRequest: (InAppWebViewController controller, String origin, List resources) async { return PermissionRequestResponse(resources: resources, action: PermissionRequestResponseAction.GRANT); }, - shouldOverrideUrlLoading: (controller, shouldOverrideUrlLoadingRequest) async { - var url = shouldOverrideUrlLoadingRequest.url; - var uri = Uri.parse(url); + shouldOverrideUrlLoading: (controller, navigationAction) async { + var uri = navigationAction.request.url!; if (!["http", "https", "file", "chrome", "data", "javascript", @@ -134,17 +134,21 @@ class _InAppWebViewExampleScreenState extends State { url, ); // and cancel the request - return ShouldOverrideUrlLoadingAction.CANCEL; + return NavigationActionPolicy.CANCEL; } } - return ShouldOverrideUrlLoadingAction.ALLOW; + return NavigationActionPolicy.ALLOW; + }, + onLoadResource: (controller, resource) { + // print(resource); }, onLoadStop: (controller, url) async { print("onLoadStop $url"); setState(() { - this.url = url ?? ''; + this.url = url.toString(); }); + webView = controller; // RenderObject renderBox = webViewKey.currentContext!.findRenderObject()!; // print(renderBox.paintBounds.size); @@ -157,10 +161,11 @@ class _InAppWebViewExampleScreenState extends State { onUpdateVisitedHistory: (controller, url, androidIsReload) { print("onUpdateVisitedHistory $url"); setState(() { - this.url = url ?? ''; + this.url = url.toString(); }); }, onConsoleMessage: (controller, consoleMessage) { + print("CONSOLE MESSAGE FROM MAIN WEBVIEW!"); print(consoleMessage); }, ), @@ -169,19 +174,19 @@ class _InAppWebViewExampleScreenState extends State { ButtonBar( alignment: MainAxisAlignment.center, children: [ - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_back), onPressed: () { webView?.goBack(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.arrow_forward), onPressed: () { webView?.goForward(); }, ), - RaisedButton( + ElevatedButton( child: Icon(Icons.refresh), onPressed: () { webView?.reload(); diff --git a/example/test_assets/in_app_webview_on_load_resource_test.html b/example/test_assets/in_app_webview_on_load_resource_test.html index bb3ee318..957c9b6b 100755 --- a/example/test_assets/in_app_webview_on_load_resource_test.html +++ b/example/test_assets/in_app_webview_on_load_resource_test.html @@ -5,9 +5,7 @@ InAppWebViewOnLoadResourceTest - - @@ -20,5 +18,17 @@

+ \ No newline at end of file diff --git a/flutter_inappwebview.iml b/flutter_inappwebview.iml index 0adae5aa..4cb39159 100755 --- a/flutter_inappwebview.iml +++ b/flutter_inappwebview.iml @@ -80,5 +80,6 @@ +
\ No newline at end of file diff --git a/ios/Classes/CredentialDatabase.swift b/ios/Classes/CredentialDatabase.swift index 16a296a3..d4c4f64c 100755 --- a/ios/Classes/CredentialDatabase.swift +++ b/ios/Classes/CredentialDatabase.swift @@ -32,29 +32,16 @@ class CredentialDatabase: NSObject, FlutterPlugin { case "getAllAuthCredentials": var allCredentials: [[String: Any?]] = [] for (protectionSpace, credentials) in CredentialDatabase.credentialStore!.allCredentials { - let protectionSpaceDict = [ - "host": protectionSpace.host, - "protocol": protectionSpace.protocol, - "realm": protectionSpace.realm, - "port": protectionSpace.port - ] as [String : Any?] - - var crendentials: [[String: String?]] = [] + var crendentials: [[String: Any?]] = [] for c in credentials { - if let username = c.value.user, let password = c.value.password { - let credential: [String: String] = [ - "username": username, - "password": password, - ] - crendentials.append(credential) - } + let credential: [String: Any?] = c.value.toMap() + crendentials.append(credential) } - if crendentials.count > 0 { - let dict = [ - "protectionSpace": protectionSpaceDict, + let dict: [String : Any] = [ + "protectionSpace": protectionSpace.toMap(), "credentials": crendentials - ] as [String : Any] + ] allCredentials.append(dict) } } result(allCredentials) @@ -67,19 +54,13 @@ class CredentialDatabase: NSObject, FlutterPlugin { if let r = realm, r.isEmpty { realm = nil } - var crendentials: [[String: String?]] = [] + var crendentials: [[String: Any?]] = [] for (protectionSpace, credentials) in CredentialDatabase.credentialStore!.allCredentials { if protectionSpace.host == host && protectionSpace.realm == realm && protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { for c in credentials { - if let username = c.value.user, let password = c.value.password { - let credential: [String: String] = [ - "username": username, - "password": password, - ] - crendentials.append(credential) - } + crendentials.append(c.value.toMap()) } break } @@ -97,7 +78,8 @@ class CredentialDatabase: NSObject, FlutterPlugin { let username = arguments!["username"] as! String let password = arguments!["password"] as! String let credential = URLCredential(user: username, password: password, persistence: .permanent) - CredentialDatabase.credentialStore!.set(credential, for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) + CredentialDatabase.credentialStore!.set(credential, + for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) result(true) break case "removeHttpAuthCredential": diff --git a/ios/Classes/InAppWebView/CustomeSchemeHandler.swift b/ios/Classes/CustomeSchemeHandler.swift similarity index 83% rename from ios/Classes/InAppWebView/CustomeSchemeHandler.swift rename to ios/Classes/CustomeSchemeHandler.swift index 4626b7b1..a1121e33 100755 --- a/ios/Classes/InAppWebView/CustomeSchemeHandler.swift +++ b/ios/Classes/CustomeSchemeHandler.swift @@ -16,8 +16,8 @@ class CustomeSchemeHandler : NSObject, WKURLSchemeHandler { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { schemeHandlers[urlSchemeTask.hash] = urlSchemeTask let inAppWebView = webView as! InAppWebView - if let url = urlSchemeTask.request.url, let scheme = url.scheme { - inAppWebView.onLoadResourceCustomScheme(scheme: scheme, url: url.absoluteString, result: {(result) -> Void in + if let url = urlSchemeTask.request.url { + inAppWebView.onLoadResourceCustomScheme(url: url.absoluteString, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") } @@ -26,7 +26,7 @@ class CustomeSchemeHandler : NSObject, WKURLSchemeHandler { let json: [String: Any] if let r = result { json = r as! [String: Any] - let urlResponse = URLResponse(url: url, mimeType: (json["content-type"] as! String), expectedContentLength: -1, textEncodingName: (json["content-encoding"] as! String)) + let urlResponse = URLResponse(url: url, mimeType: (json["contentType"] as! String), expectedContentLength: -1, textEncodingName: (json["contentEncoding"] as! String)) let data = json["data"] as! FlutterStandardTypedData if (self.schemeHandlers[urlSchemeTask.hash] != nil) { urlSchemeTask.didReceive(urlResponse) diff --git a/ios/Classes/HttpAuthenticationChallenge.swift b/ios/Classes/HttpAuthenticationChallenge.swift new file mode 100644 index 00000000..e1feaa6b --- /dev/null +++ b/ios/Classes/HttpAuthenticationChallenge.swift @@ -0,0 +1,34 @@ +// +// HttpAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +class HttpAuthenticationChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + var previousFailureCount: Int = 0 + var failureResponse: URLResponse? + var error: Error? + var proposedCredential: URLCredential? + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + previousFailureCount = fromChallenge.previousFailureCount + failureResponse = fromChallenge.failureResponse + error = fromChallenge.error + proposedCredential = fromChallenge.proposedCredential + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "iosFailureResponse": failureResponse?.toMap(), + "iosError": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap() + ] + } +} diff --git a/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift b/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift new file mode 100644 index 00000000..ba013c58 --- /dev/null +++ b/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift @@ -0,0 +1,17 @@ +// +// InAppBrowserDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public protocol InAppBrowserDelegate { + func didChangeTitle(title: String?) + func didStartNavigation(url: URL?) + func didUpdateVisitedHistory(url: URL?) + func didFinishNavigation(url: URL?) + func didFailNavigation(url: URL?, error: Error) + func didChangeProgress(progress: Double) +} diff --git a/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ios/Classes/InAppBrowser/InAppBrowserManager.swift index 3d006191..bd30a429 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserManager.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -13,12 +13,12 @@ import AVFoundation let WEBVIEW_STORYBOARD = "WebView" let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" +let NAV_STORYBOARD_CONTROLLER_ID = "navController" public class InAppBrowserManager: NSObject, FlutterPlugin { static var registrar: FlutterPluginRegistrar? static var channel: FlutterMethodChannel? - // var tmpWindow: UIWindow? private var previousStatusBarStyle = -1 public static func register(with registrar: FlutterPluginRegistrar) { @@ -36,34 +36,24 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { let arguments = call.arguments as? NSDictionary switch call.method { - case "openUrl": + case "openUrlRequest": let uuid = arguments!["uuid"] as! String - let url = arguments!["url"] as! String + let urlRequest = arguments!["urlRequest"] as! [String:Any?] let options = arguments!["options"] as! [String: Any?] - let headers = arguments!["headers"] as! [String: String] let contextMenu = arguments!["contextMenu"] as! [String: Any] let windowId = arguments!["windowId"] as? Int64 let initialUserScripts = arguments!["initialUserScripts"] as? [[String: Any]] - openUrl(uuid: uuid, url: url, options: options, headers: headers, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) + openUrlRequest(uuid: uuid, urlRequest: urlRequest, options: options, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) result(true) break case "openFile": let uuid = arguments!["uuid"] as! String - var url = arguments!["url"] as! String - let key = InAppBrowserManager.registrar!.lookupKey(forAsset: url) - let assetURL = Bundle.main.url(forResource: key, withExtension: nil) - if assetURL == nil { - result(FlutterError(code: "InAppBrowserFlutterPlugin", message: url + " asset file cannot be found!", details: nil)) - return - } else { - url = assetURL!.absoluteString - } + let assetFilePath = arguments!["assetFilePath"] as! String let options = arguments!["options"] as! [String: Any?] - let headers = arguments!["headers"] as! [String: String] let contextMenu = arguments!["contextMenu"] as! [String: Any] let windowId = arguments!["windowId"] as? Int64 let initialUserScripts = arguments!["initialUserScripts"] as? [[String: Any]] - openUrl(uuid: uuid, url: url, options: options, headers: headers, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) + openFile(uuid: uuid, assetFilePath: assetFilePath, options: options, contextMenu: contextMenu, windowId: windowId, initialUserScripts: initialUserScripts) result(true) break case "openData": @@ -91,10 +81,72 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { } public func prepareInAppBrowserWebViewController(options: [String: Any?]) -> InAppBrowserWebViewController { - if self.previousStatusBarStyle == -1 { - self.previousStatusBarStyle = UIApplication.shared.statusBarStyle.rawValue + if previousStatusBarStyle == -1 { + previousStatusBarStyle = UIApplication.shared.statusBarStyle.rawValue } + let browserOptions = InAppBrowserOptions() + let _ = browserOptions.parse(options: options) + + let webViewOptions = InAppWebViewOptions() + let _ = webViewOptions.parse(options: options) + + let webViewController = InAppBrowserWebViewController() + webViewController.browserOptions = browserOptions + webViewController.webViewOptions = webViewOptions + webViewController.previousStatusBarStyle = previousStatusBarStyle + return webViewController + } + + public func openUrlRequest(uuid: String, urlRequest: [String:Any?], options: [String: Any?], + contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { + let webViewController = prepareInAppBrowserWebViewController(options: options) + + webViewController.uuid = uuid + webViewController.initialUrlRequest = URLRequest.init(fromPluginMap: urlRequest) + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + + presentViewController(webViewController: webViewController) + } + + public func openFile(uuid: String, assetFilePath: String, options: [String: Any?], + contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { + let webViewController = prepareInAppBrowserWebViewController(options: options) + + webViewController.uuid = uuid + webViewController.initialFile = assetFilePath + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + + presentViewController(webViewController: webViewController) + } + + public func openData(uuid: String, options: [String: Any?], data: String, mimeType: String, encoding: String, + baseUrl: String, contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { + let webViewController = prepareInAppBrowserWebViewController(options: options) + + webViewController.uuid = uuid + webViewController.initialData = data + webViewController.initialMimeType = mimeType + webViewController.initialEncoding = encoding + webViewController.initialBaseUrl = baseUrl + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + + presentViewController(webViewController: webViewController) + } + + public func presentViewController(webViewController: InAppBrowserWebViewController) { + let storyboard = UIStoryboard(name: WEBVIEW_STORYBOARD, bundle: Bundle(for: InAppWebViewFlutterPlugin.self)) + let navController = storyboard.instantiateViewController(withIdentifier: NAV_STORYBOARD_CONTROLLER_ID) as! InAppBrowserNavigationController + webViewController.edgesForExtendedLayout = [] + navController.pushViewController(webViewController, animated: false) + webViewController.prepareNavigationControllerBeforeViewWillAppear() + let frame: CGRect = UIScreen.main.bounds let tmpWindow = UIWindow(frame: frame) @@ -103,81 +155,15 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { tmpWindow.rootViewController = tmpController tmpWindow.windowLevel = UIWindow.Level(baseWindowLevel!.rawValue + 1.0) tmpWindow.makeKeyAndVisible() + navController.tmpWindow = tmpWindow - let browserOptions = InAppBrowserOptions() - let _ = browserOptions.parse(options: options) - - let webViewOptions = InAppWebViewOptions() - let _ = webViewOptions.parse(options: options) - - let storyboard = UIStoryboard(name: WEBVIEW_STORYBOARD, bundle: Bundle(for: InAppWebViewFlutterPlugin.self)) - let webViewController = storyboard.instantiateViewController(withIdentifier: WEBVIEW_STORYBOARD_CONTROLLER_ID) as! InAppBrowserWebViewController - webViewController.tmpWindow = tmpWindow - webViewController.browserOptions = browserOptions - webViewController.webViewOptions = webViewOptions - webViewController.isHidden = browserOptions.hidden - webViewController.previousStatusBarStyle = previousStatusBarStyle - webViewController.prepareBeforeViewWillAppear() - return webViewController - } - - public func openUrl(uuid: String, url: String, options: [String: Any?], headers: [String: String], - contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { - let absoluteUrl = URL(string: url)!.absoluteURL - let webViewController = prepareInAppBrowserWebViewController(options: options) - - webViewController.uuid = uuid - webViewController.initURL = absoluteUrl - webViewController.initHeaders = headers - webViewController.contextMenu = contextMenu - webViewController.windowId = windowId - webViewController.initUserScripts = initialUserScripts ?? [] - - if webViewController.isHidden { - webViewController.view.isHidden = true - webViewController.tmpWindow!.rootViewController!.present(webViewController, animated: false, completion: {() -> Void in - - }) - webViewController.presentingViewController?.dismiss(animated: false, completion: {() -> Void in - webViewController.tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) - UIApplication.shared.delegate?.window??.makeKeyAndVisible() - }) - } - else { - webViewController.tmpWindow!.rootViewController!.present(webViewController, animated: true, completion: {() -> Void in - - }) - } - } - - public func openData(uuid: String, options: [String: Any?], data: String, mimeType: String, encoding: String, - baseUrl: String, contextMenu: [String: Any], windowId: Int64?, initialUserScripts: [[String: Any]]?) { - let webViewController = prepareInAppBrowserWebViewController(options: options) - - webViewController.uuid = uuid - webViewController.initData = data - webViewController.initMimeType = mimeType - webViewController.initEncoding = encoding - webViewController.initBaseUrl = baseUrl - webViewController.contextMenu = contextMenu - webViewController.windowId = windowId - webViewController.initUserScripts = initialUserScripts ?? [] - - if webViewController.isHidden { - webViewController.view.isHidden = true - webViewController.tmpWindow!.rootViewController!.present(webViewController, animated: false, completion: {() -> Void in - - }) - webViewController.presentingViewController?.dismiss(animated: false, completion: {() -> Void in - webViewController.tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) - UIApplication.shared.delegate?.window??.makeKeyAndVisible() - }) - } - else { - webViewController.tmpWindow!.rootViewController!.present(webViewController, animated: true, completion: {() -> Void in - - }) + var animated = true + if let browserOptions = webViewController.browserOptions, browserOptions.hidden { + tmpWindow.isHidden = true + UIApplication.shared.delegate?.window??.makeKeyAndVisible() + animated = false } + tmpWindow.rootViewController!.present(navController, animated: animated, completion: nil) } public func openWithSystemBrowser(url: String, result: @escaping FlutterResult) { @@ -188,7 +174,7 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { } else { if #available(iOS 10.0, *) { - UIApplication.shared.open(absoluteUrl, options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), completionHandler: nil) + UIApplication.shared.open(absoluteUrl, options: [:], completionHandler: nil) } else { UIApplication.shared.openURL(absoluteUrl) } @@ -196,8 +182,3 @@ public class InAppBrowserManager: NSObject, FlutterPlugin { result(true) } } - -// Helper function inserted by Swift 4.2 migrator. -fileprivate func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) -> [UIApplication.OpenExternalURLOptionsKey: Any] { - return Dictionary(uniqueKeysWithValues: input.map { key, value in (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value)}) -} diff --git a/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift b/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift new file mode 100644 index 00000000..b311114a --- /dev/null +++ b/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift @@ -0,0 +1,19 @@ +// +// InAppBrowserNavigationController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public class InAppBrowserNavigationController: UINavigationController { + var tmpWindow: UIWindow? + + deinit { + print("InAppBrowserNavigationController - dealloc") + tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) + tmpWindow = nil + UIApplication.shared.delegate?.window??.makeKeyAndVisible() + } +} diff --git a/ios/Classes/InAppBrowser/InAppBrowserOptions.swift b/ios/Classes/InAppBrowser/InAppBrowserOptions.swift index 0eee78d3..4f475430 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserOptions.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserOptions.swift @@ -11,19 +11,22 @@ import Foundation public class InAppBrowserOptions: Options { var hidden = false - var toolbarTop = true - var toolbarTopBackgroundColor = "" - var toolbarTopFixedTitle = "" + var hideToolbarTop = true + var toolbarTopBackgroundColor: String? var hideUrlBar = false + var hideProgressBar = false - var toolbarBottom = true - var toolbarBottomBackgroundColor = "" + var toolbarTopTranslucent = true + var toolbarTopBarTintColor: String? + var toolbarTopTintColor: String? + var hideToolbarBottom = true + var toolbarBottomBackgroundColor: String? + var toolbarBottomTintColor: String? var toolbarBottomTranslucent = true - var closeButtonCaption = "" - var closeButtonColor = "" + var closeButtonCaption: String? + var closeButtonColor: String? var presentationStyle = 0 //fullscreen var transitionStyle = 0 //crossDissolve - var spinner = true override init(){ super.init() @@ -32,8 +35,23 @@ public class InAppBrowserOptions: Options { override func getRealOptions(obj: InAppBrowserWebViewController?) -> [String: Any?] { var realOptions: [String: Any?] = toMap() if let inAppBrowserWebViewController = obj { - realOptions["presentationStyle"] = inAppBrowserWebViewController.modalPresentationStyle.rawValue - realOptions["transitionStyle"] = inAppBrowserWebViewController.modalTransitionStyle.rawValue + realOptions["hideUrlBar"] = inAppBrowserWebViewController.searchBar.isHidden + realOptions["hideUrlBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["closeButtonCaption"] = inAppBrowserWebViewController.closeButton.title + realOptions["closeButtonColor"] = inAppBrowserWebViewController.closeButton.tintColor?.hexString + if let navController = inAppBrowserWebViewController.navigationController { + realOptions["hideToolbarTop"] = navController.navigationBar.isHidden + realOptions["toolbarTopBackgroundColor"] = navController.navigationBar.backgroundColor?.hexString + realOptions["toolbarTopTranslucent"] = navController.navigationBar.isTranslucent + realOptions["toolbarTopBarTintColor"] = navController.navigationBar.barTintColor?.hexString + realOptions["toolbarTopTintColor"] = navController.navigationBar.tintColor?.hexString + realOptions["hideToolbarBottom"] = navController.toolbar.isHidden + realOptions["toolbarBottomBackgroundColor"] = navController.toolbar.barTintColor?.hexString + realOptions["toolbarBottomTranslucent"] = navController.toolbar.isTranslucent + realOptions["toolbarBottomTintColor"] = navController.toolbar.tintColor?.hexString + realOptions["presentationStyle"] = navController.modalPresentationStyle.rawValue + realOptions["transitionStyle"] = navController.modalTransitionStyle.rawValue + } } return realOptions } diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index ae6c3974..4997aa4a 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -9,401 +9,402 @@ import Flutter import UIKit import WebKit import Foundation -import AVFoundation -typealias OlderClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void -typealias NewerClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void - -public class InAppWebView_IBWrapper: UIView { - required init?(coder: NSCoder) { - super.init(coder: coder) - self.translatesAutoresizingMaskIntoConstraints = false - } -} - -public class InAppBrowserWebViewController: UIViewController, UIScrollViewDelegate, WKUIDelegate, UITextFieldDelegate { +public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelegate, UIScrollViewDelegate, WKUIDelegate, UISearchBarDelegate { - @IBOutlet var containerWebView: InAppWebView_IBWrapper! - @IBOutlet var closeButton: UIButton! - @IBOutlet var reloadButton: UIBarButtonItem! - @IBOutlet var backButton: UIBarButtonItem! - @IBOutlet var forwardButton: UIBarButtonItem! - @IBOutlet var shareButton: UIBarButtonItem! - @IBOutlet var spinner: UIActivityIndicatorView! - @IBOutlet var toolbarTop: UIView! - @IBOutlet var toolbarBottom: UIToolbar! - @IBOutlet var urlField: UITextField! - - @IBOutlet var toolbarTop_BottomToWebViewTopConstraint: NSLayoutConstraint! - @IBOutlet var toolbarBottom_TopToWebViewBottomConstraint: NSLayoutConstraint! - @IBOutlet var containerWebView_BottomFullScreenConstraint: NSLayoutConstraint! - @IBOutlet var containerWebView_TopFullScreenConstraint: NSLayoutConstraint! - @IBOutlet var webView_BottomFullScreenConstraint: NSLayoutConstraint! - @IBOutlet var webView_TopFullScreenConstraint: NSLayoutConstraint! + var closeButton: UIBarButtonItem! + var reloadButton: UIBarButtonItem! + var backButton: UIBarButtonItem! + var forwardButton: UIBarButtonItem! + var shareButton: UIBarButtonItem! + var searchBar: UISearchBar! + var progressBar: UIProgressView! + var tmpWindow: UIWindow? var uuid: String = "" var windowId: Int64? var webView: InAppWebView! var channel: FlutterMethodChannel? - var initURL: URL? + var initialUrlRequest: URLRequest? + var initialFile: String? var contextMenu: [String: Any]? - var tmpWindow: UIWindow? var browserOptions: InAppBrowserOptions? var webViewOptions: InAppWebViewOptions? - var initHeaders: [String: String]? - var initData: String? - var initMimeType: String? - var initEncoding: String? - var initBaseUrl: String? - var isHidden = false - var viewPrepared = false + var initialData: String? + var initialMimeType: String? + var initialEncoding: String? + var initialBaseUrl: String? var previousStatusBarStyle = -1 - var initUserScripts: [[String: Any]] = [] + var initialUserScripts: [[String: Any]] = [] var methodCallDelegate: InAppWebViewMethodHandler? - - required init(coder aDecoder: NSCoder) { - super.init(coder: aDecoder)! - } - - public override func viewWillAppear(_ animated: Bool) { - if !viewPrepared { - channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappbrowser_" + uuid, binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) - - let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(options: webViewOptions) - if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { - webView = webViewTransport.webView - webView.IABController = self - webView.contextMenu = contextMenu - webView.channel = channel! - } else { - webView = InAppWebView(frame: .zero, - configuration: preWebviewConfiguration, - IABController: self, - contextMenu: contextMenu, - channel: channel!) - } - - methodCallDelegate = InAppWebViewMethodHandler(webView: webView!) - channel!.setMethodCallHandler(LeakAvoider(delegate: methodCallDelegate!).handle) - - webView.appendUserScripts(userScripts: initUserScripts) - containerWebView.addSubview(webView) - prepareConstraints() - prepareWebView() - - if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { - self.webView.load(webViewTransport.request) - } else { - if #available(iOS 11.0, *) { - if let contentBlockers = webView.options?.contentBlockers, contentBlockers.count > 0 { - do { - let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) - let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) - WKContentRuleListStore.default().compileContentRuleList( - forIdentifier: "ContentBlockingRules", - encodedContentRuleList: blockRules) { (contentRuleList, error) in - if let error = error { - print(error.localizedDescription) - return - } - - let configuration = self.webView!.configuration - configuration.userContentController.add(contentRuleList!) - - self.initLoad() - - self.onBrowserCreated() - } - return - } catch { - print(error.localizedDescription) - } - } - } - - initLoad() - } - - onBrowserCreated() + public override func loadView() { + channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappbrowser_" + uuid, binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) + + var userScripts: [UserScript] = [] + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) } - viewPrepared = true - super.viewWillAppear(animated) - } - - public func initLoad() { - if initData == nil, let initURL = initURL { - var allowingReadAccessToURL: URL? = nil - if let allowingReadAccessTo = webView.options?.allowingReadAccessTo, initURL.scheme == "file" { - allowingReadAccessToURL = URL(string: allowingReadAccessTo) - if allowingReadAccessToURL?.scheme != "file" { - allowingReadAccessToURL = nil - } - } - loadUrl(url: initURL, headers: initHeaders, allowingReadAccessTo: allowingReadAccessToURL) - } - else { - webView.loadData(data: initData!, mimeType: initMimeType!, encoding: initEncoding!, baseUrl: initBaseUrl!) + + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(options: webViewOptions) + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView = webViewTransport.webView + webView.contextMenu = contextMenu + webView.channel = channel! + webView.initialUserScripts = userScripts + } else { + webView = InAppWebView(frame: .zero, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + channel: channel!, + userScripts: userScripts) } + webView.inAppBrowserDelegate = self + + methodCallDelegate = InAppWebViewMethodHandler(webView: webView!) + channel!.setMethodCallHandler(LeakAvoider(delegate: methodCallDelegate!).handle) + + prepareWebView() + + progressBar = UIProgressView(progressViewStyle: .bar) + + view = UIView() + view.addSubview(webView) + view.insertSubview(progressBar, aboveSubview: webView) } public override func viewDidLoad() { super.viewDidLoad() - urlField.delegate = self - urlField.text = initURL?.absoluteString - urlField.backgroundColor = .white - urlField.textColor = .black - urlField.layer.borderWidth = 1.0 - urlField.layer.borderColor = UIColor.lightGray.cgColor - urlField.layer.cornerRadius = 4 + webView.translatesAutoresizingMaskIntoConstraints = false + progressBar.translatesAutoresizingMaskIntoConstraints = false - closeButton.addTarget(self, action: #selector(close), for: .touchUpInside) + if #available(iOS 9.0, *) { + webView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0).isActive = true + webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + progressBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + progressBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + progressBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + } else { + view.addConstraints([ + NSLayoutConstraint(item: webView!, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView!, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView!, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView!, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0) + ]) + + view.addConstraints([ + NSLayoutConstraint(item: progressBar!, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0), + NSLayoutConstraint(item: progressBar!, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0), + NSLayoutConstraint(item: progressBar!, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0) + ]) + } - forwardButton.target = self - forwardButton.action = #selector(goForward) - - forwardButton.target = self - forwardButton.action = #selector(goForward) - - backButton.target = self - backButton.action = #selector(goBack) - - reloadButton.target = self - reloadButton.action = #selector(reload) - - shareButton.target = self - shareButton.action = #selector(share) - - spinner.hidesWhenStopped = true - spinner.isHidden = false - spinner.stopAnimating() + if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { + webView.load(webViewTransport.request) + } else { + if #available(iOS 11.0, *) { + if let contentBlockers = webView.options?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: String.Encoding.utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = self.webView!.configuration + configuration.userContentController.add(contentRuleList!) + + self.initLoad() + } + return + } catch { + print(error.localizedDescription) + } + } + } + + initLoad() + } + } + + public func initLoad() { + if let initialFile = initialFile { + do { + try webView?.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData { + webView.loadData(data: initialData, mimeType: initialMimeType!, encoding: initialEncoding!, baseUrl: initialBaseUrl!) + } + else if let initialUrlRequest = initialUrlRequest { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.options?.allowingReadAccessTo, let url = initialUrlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadUrl(urlRequest: initialUrlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + onBrowserCreated() } - // Prevent crashes on closing windows deinit { print("InAppBrowserWebViewController - dealloc") } - public override func viewWillDisappear (_ animated: Bool) { + public override func viewDidDisappear(_ animated: Bool) { dispose() + super.viewDidDisappear(animated) + } + + public override func viewWillDisappear (_ animated: Bool) { super.viewWillDisappear(animated) } - public func prepareConstraints () { - containerWebView_BottomFullScreenConstraint = NSLayoutConstraint(item: containerWebView!, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: view, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1, constant: 0) - containerWebView_TopFullScreenConstraint = NSLayoutConstraint(item: containerWebView!, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0) - - webView.translatesAutoresizingMaskIntoConstraints = false - let height = NSLayoutConstraint(item: webView!, attribute: .height, relatedBy: .equal, toItem: containerWebView, attribute: .height, multiplier: 1, constant: 0) - let width = NSLayoutConstraint(item: webView!, attribute: .width, relatedBy: .equal, toItem: containerWebView, attribute: .width, multiplier: 1, constant: 0) - let leftConstraint = NSLayoutConstraint(item: webView!, attribute: .leftMargin, relatedBy: .equal, toItem: containerWebView, attribute: .leftMargin, multiplier: 1, constant: 0) - let rightConstraint = NSLayoutConstraint(item: webView!, attribute: .rightMargin, relatedBy: .equal, toItem: containerWebView, attribute: .rightMargin, multiplier: 1, constant: 0) - let bottomContraint = NSLayoutConstraint(item: webView!, attribute: .bottomMargin, relatedBy: .equal, toItem: containerWebView, attribute: .bottomMargin, multiplier: 1, constant: 0) - containerWebView.addConstraints([height, width, leftConstraint, rightConstraint, bottomContraint]) - - webView_BottomFullScreenConstraint = NSLayoutConstraint(item: webView!, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: containerWebView, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1, constant: 0) - webView_TopFullScreenConstraint = NSLayoutConstraint(item: webView!, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: containerWebView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0) + public func prepareNavigationControllerBeforeViewWillAppear() { + if let browserOptions = browserOptions { + navigationController?.modalPresentationStyle = UIModalPresentationStyle(rawValue: browserOptions.presentationStyle)! + navigationController?.modalTransitionStyle = UIModalTransitionStyle(rawValue: browserOptions.transitionStyle)! + } } public func prepareWebView() { webView.options = webViewOptions webView.prepare() + + searchBar = UISearchBar() + searchBar.keyboardType = .URL + searchBar.sizeToFit() + searchBar.delegate = self + navigationItem.titleView = searchBar - if (browserOptions?.hideUrlBar)! { - urlField.isHidden = true - urlField.isEnabled = false + let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + reloadButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reload)) + shareButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) + forwardButton = UIBarButtonItem(title: "\u{203A}", style: .plain, target: self, action: #selector(goForward)) + forwardButton.isEnabled = false + backButton = UIBarButtonItem(title: "\u{2039}", style: .plain, target: self, action: #selector(goBack)) + backButton.isEnabled = false + + for state: UIControl.State in [.normal, .disabled, .highlighted, .selected] { + forwardButton.setTitleTextAttributes([ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 50.0), + NSAttributedString.Key.baselineOffset: 2.5 + ], for: state) + backButton.setTitleTextAttributes([ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 50.0), + NSAttributedString.Key.baselineOffset: 2.5 + ], for: state) } - if (browserOptions?.toolbarTop)! { - if browserOptions?.toolbarTopBackgroundColor != "" { - toolbarTop.backgroundColor = color(fromHexString: (browserOptions?.toolbarTopBackgroundColor)!) + toolbarItems = [backButton, spacer, forwardButton, spacer, shareButton, spacer, reloadButton] + + if let browserOptions = browserOptions { + if !browserOptions.hideToolbarTop { + navigationController?.navigationBar.isHidden = false + if browserOptions.hideUrlBar { + searchBar.isHidden = true + } + if let bgColor = browserOptions.toolbarTopBackgroundColor, !bgColor.isEmpty { + navigationController?.navigationBar.backgroundColor = UIColor(hexString: bgColor) + } + if let barTintColor = browserOptions.toolbarTopBarTintColor, !barTintColor.isEmpty { + navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) + } + if let tintColor = browserOptions.toolbarTopTintColor, !tintColor.isEmpty { + navigationController?.navigationBar.barTintColor = UIColor(hexString: tintColor) + } + navigationController?.navigationBar.isTranslucent = browserOptions.toolbarTopTranslucent + } + else { + navigationController?.navigationBar.isHidden = true + } + + if !browserOptions.hideToolbarBottom { + navigationController?.isToolbarHidden = false + if let bgColor = browserOptions.toolbarBottomBackgroundColor, !bgColor.isEmpty { + navigationController?.toolbar.barTintColor = UIColor(hexString: bgColor) + } + if let tintColor = browserOptions.toolbarBottomTintColor, !tintColor.isEmpty { + navigationController?.toolbar.tintColor = UIColor(hexString: tintColor) + } + navigationController?.toolbar.isTranslucent = false + } + else { + navigationController?.isToolbarHidden = true + } + + if let closeButtonCaption = browserOptions.closeButtonCaption, !closeButtonCaption.isEmpty { + closeButton = UIBarButtonItem(title: closeButtonCaption, style: .plain, target: self, action: #selector(close)) + } else { + setDefaultCloseButton() + } + + if let closeButtonColor = browserOptions.closeButtonColor, !closeButtonColor.isEmpty { + closeButton.tintColor = UIColor(hexString: closeButtonColor) + } + + if browserOptions.hideProgressBar { + progressBar.isHidden = true } } - else { - toolbarTop.isHidden = true - toolbarTop_BottomToWebViewTopConstraint.isActive = false - containerWebView_TopFullScreenConstraint.isActive = true - webView_TopFullScreenConstraint.isActive = true - } - if (browserOptions?.toolbarBottom)! { - if browserOptions?.toolbarBottomBackgroundColor != "" { - toolbarBottom.backgroundColor = color(fromHexString: (browserOptions?.toolbarBottomBackgroundColor)!) + navigationItem.rightBarButtonItem = closeButton + } + + func setDefaultCloseButton() { + if closeButton != nil { + closeButton.target = nil + closeButton.action = nil + } + if #available(iOS 13.0, *) { + closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(close)) + } else { + closeButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(close)) + } + } + + public func didChangeTitle(title: String?) { + guard let _ = title else { + return + } + } + + public func didStartNavigation(url: URL?) { + forwardButton.isEnabled = webView.canGoForward + backButton.isEnabled = webView.canGoBack + progressBar.setProgress(0.0, animated: false) + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didUpdateVisitedHistory(url: URL?) { + forwardButton.isEnabled = webView.canGoForward + backButton.isEnabled = webView.canGoBack + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didFinishNavigation(url: URL?) { + forwardButton.isEnabled = webView.canGoForward + backButton.isEnabled = webView.canGoBack + progressBar.setProgress(0.0, animated: false) + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didFailNavigation(url: URL?, error: Error) { + forwardButton.isEnabled = webView.canGoForward + backButton.isEnabled = webView.canGoBack + progressBar.setProgress(0.0, animated: false) + } + + public func didChangeProgress(progress: Double) { + progressBar.setProgress(Float(progress), animated: true) + } + + public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + guard let text = searchBar.text, + let urlEncoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: urlEncoded) else { + return + } + let request = URLRequest(url: url) + webView.load(request) + } + + public func show(completion: (() -> Void)? = nil) { + if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + window.alpha = 0.0 + window.isHidden = false + window.makeKeyAndVisible() + UIView.animate(withDuration: 0.2) { + window.alpha = 1.0 + completion?() } - toolbarBottom.isTranslucent = (browserOptions?.toolbarBottomTranslucent)! } - else { - toolbarBottom.isHidden = true - toolbarBottom_TopToWebViewBottomConstraint.isActive = false - containerWebView_BottomFullScreenConstraint.isActive = true - webView_BottomFullScreenConstraint.isActive = true - } - - if browserOptions?.closeButtonCaption != "" { - closeButton.setTitle(browserOptions?.closeButtonCaption, for: .normal) - } - if browserOptions?.closeButtonColor != "" { - closeButton.tintColor = color(fromHexString: (browserOptions?.closeButtonColor)!) - } - } - - public func prepareBeforeViewWillAppear() { - modalPresentationStyle = UIModalPresentationStyle(rawValue: (browserOptions?.presentationStyle)!)! - modalTransitionStyle = UIModalTransitionStyle(rawValue: (browserOptions?.transitionStyle)!)! - } - - public func loadUrl(url: URL, headers: [String: String]?, allowingReadAccessTo: URL?) { - webView.loadUrl(url: url, headers: headers, allowingReadAccessTo: allowingReadAccessTo) - updateUrlTextField(url: (webView.currentURL?.absoluteString)!) - } - - // Load user requested url - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - if textField.text != nil && textField.text != "" { - let url = textField.text?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - let request = URLRequest(url: URL(string: url!)!) - webView.load(request) - } - else { - updateUrlTextField(url: (webView.currentURL?.absoluteString)!) - } - return false - } - - func setWebViewFrame(_ frame: CGRect) { - print("Setting the WebView's frame to \(NSCoder.string(for: frame))") - webView.frame = frame - } - - public func show() { - isHidden = false - view.isHidden = false - - // Run later to avoid the "took a long time" log message. - DispatchQueue.main.async(execute: {() -> Void in - let baseWindowLevel = UIApplication.shared.keyWindow?.windowLevel - self.tmpWindow?.windowLevel = UIWindow.Level(baseWindowLevel!.rawValue + 1.0) - self.tmpWindow?.makeKeyAndVisible() - UIApplication.shared.delegate?.window??.makeKeyAndVisible() - self.tmpWindow?.rootViewController?.present(self, animated: true, completion: nil) - }) } - public func hide() { - isHidden = true - - // Run later to avoid the "took a long time" log message. - DispatchQueue.main.async(execute: {() -> Void in - self.presentingViewController?.dismiss(animated: true, completion: {() -> Void in - self.tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) - UIApplication.shared.delegate?.window??.makeKeyAndVisible() - if self.previousStatusBarStyle != -1 { - UIApplication.shared.statusBarStyle = UIStatusBarStyle(rawValue: self.previousStatusBarStyle)! + public func hide(completion: (() -> Void)? = nil) { + if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + window.alpha = 1.0 + UIView.animate(withDuration: 0.2) { + window.alpha = 0.0 + } completion: { (finished) in + if finished { + window.isHidden = true + UIApplication.shared.delegate?.window??.makeKeyAndVisible() + completion?() } - }) - }) + } + } } - @objc public func reload () { + @objc public func reload() { webView.reload() + didUpdateVisitedHistory(url: webView.url) } - @objc public func share () { + @objc public func share() { let vc = UIActivityViewController(activityItems: [webView.currentURL ?? ""], applicationActivities: []) present(vc, animated: true, completion: nil) } - @objc public func close() { - weak var weakSelf = self - - if (weakSelf?.responds(to: #selector(getter: self.presentingViewController)))! { - weakSelf?.presentingViewController?.dismiss(animated: true, completion: {() -> Void in - + public func close(completion: (() -> Void)? = nil) { + if (navigationController?.responds(to: #selector(getter: navigationController?.presentingViewController)))! { + navigationController?.presentingViewController?.dismiss(animated: true, completion: {() -> Void in + completion?() }) } else { - weakSelf?.parent?.dismiss(animated: true, completion: {() -> Void in - + navigationController?.parent?.dismiss(animated: true, completion: {() -> Void in + completion?() }) } } + @objc public func close() { + if (navigationController?.responds(to: #selector(getter: navigationController?.presentingViewController)))! { + navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) + } + else { + navigationController?.parent?.dismiss(animated: true, completion: nil) + } + } + @objc public func goBack() { - if canGoBack() { + if webView.canGoBack { webView.goBack() - updateUrlTextField(url: (webView?.url?.absoluteString)!) } } - public func canGoBack() -> Bool { - return webView.canGoBack - } - @objc public func goForward() { - if canGoForward() { + if webView.canGoForward { webView.goForward() - updateUrlTextField(url: (webView?.url?.absoluteString)!) } } - public func canGoForward() -> Bool { - return webView.canGoForward - } - @objc public func goBackOrForward(steps: Int) { webView.goBackOrForward(steps: steps) - updateUrlTextField(url: (webView?.url?.absoluteString)!) - } - - public func canGoBackOrForward(steps: Int) -> Bool { - return webView.canGoBackOrForward(steps: steps) - } - - public func updateUrlTextField(url: String) { - urlField.text = url - } - - // - // On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. - // The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't - // change that value. - // - - func getStatusBarOffset() -> Float { - let statusBarFrame: CGRect = UIApplication.shared.statusBarFrame - let statusBarOffset: Float = Float(min(statusBarFrame.size.width, statusBarFrame.size.height)) - return statusBarOffset - } - - // Helper function to convert hex color string to UIColor - // Assumes input like "#00FF00" (#RRGGBB). - // Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string - - func color(fromHexString: String, alpha:CGFloat? = 1.0) -> UIColor { - - // Convert hex string to an integer - let hexint = Int(self.intFromHexString(hexStr: fromHexString)) - let red = CGFloat((hexint & 0xff0000) >> 16) / 255.0 - let green = CGFloat((hexint & 0xff00) >> 8) / 255.0 - let blue = CGFloat((hexint & 0xff) >> 0) / 255.0 - let alpha = alpha! - - // Create color object, specifying alpha as well - let color = UIColor(red: red, green: green, blue: blue, alpha: alpha) - return color - } - - func intFromHexString(hexStr: String) -> UInt32 { - var hexInt: UInt32 = 0 - // Create scanner - let scanner: Scanner = Scanner(string: hexStr) - // Tell scanner to skip the # character - scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") - // Scan hex value - scanner.scanHexInt32(&hexInt) - return hexInt } public func setOptions(newOptions: InAppBrowserOptions, newOptionsMap: [String: Any]) { @@ -412,7 +413,7 @@ public class InAppBrowserWebViewController: UIViewController, UIScrollViewDelega let _ = newInAppWebViewOptions.parse(options: newOptionsMap) self.webView.setOptions(newOptions: newInAppWebViewOptions, newOptionsMap: newOptionsMap) - if newOptionsMap["hidden"] != nil && browserOptions?.hidden != newOptions.hidden { + if newOptionsMap["hidden"] != nil, browserOptions?.hidden != newOptions.hidden { if newOptions.hidden { hide() } @@ -421,51 +422,98 @@ public class InAppBrowserWebViewController: UIViewController, UIScrollViewDelega } } - if newOptionsMap["hideUrlBar"] != nil && browserOptions?.hideUrlBar != newOptions.hideUrlBar { - self.urlField.isHidden = newOptions.hideUrlBar - self.urlField.isEnabled = !newOptions.hideUrlBar + if newOptionsMap["hideUrlBar"] != nil, browserOptions?.hideUrlBar != newOptions.hideUrlBar { + searchBar.isHidden = newOptions.hideUrlBar + } + + if newOptionsMap["toolbarTop"] != nil, browserOptions?.hideToolbarTop != newOptions.hideToolbarTop { + navigationController?.navigationBar.isHidden = newOptions.hideToolbarTop + } + + if newOptionsMap["toolbarTopBackgroundColor"] != nil, browserOptions?.toolbarTopBackgroundColor != newOptions.toolbarTopBackgroundColor { + if let bgColor = newOptions.toolbarTopBackgroundColor, !bgColor.isEmpty { + navigationController?.navigationBar.backgroundColor = UIColor(hexString: bgColor) + } else { + navigationController?.navigationBar.backgroundColor = nil + } } - if newOptionsMap["toolbarTop"] != nil && browserOptions?.toolbarTop != newOptions.toolbarTop { - self.containerWebView_TopFullScreenConstraint.isActive = !newOptions.toolbarTop - self.webView_TopFullScreenConstraint.isActive = !newOptions.toolbarTop - self.toolbarTop.isHidden = !newOptions.toolbarTop - self.toolbarTop_BottomToWebViewTopConstraint.isActive = newOptions.toolbarTop + if newOptionsMap["toolbarTopBarTintColor"] != nil, browserOptions?.toolbarTopBarTintColor != newOptions.toolbarTopBarTintColor { + if let barTintColor = newOptions.toolbarTopBarTintColor, !barTintColor.isEmpty { + navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) + } else { + navigationController?.navigationBar.barTintColor = nil + } } - if newOptionsMap["toolbarTopBackgroundColor"] != nil && browserOptions?.toolbarTopBackgroundColor != newOptions.toolbarTopBackgroundColor && newOptions.toolbarTopBackgroundColor != "" { - self.toolbarTop.backgroundColor = color(fromHexString: newOptions.toolbarTopBackgroundColor) + if newOptionsMap["toolbarTopTintColor"] != nil, browserOptions?.toolbarTopTintColor != newOptions.toolbarTopTintColor { + if let tintColor = newOptions.toolbarTopTintColor, !tintColor.isEmpty { + navigationController?.navigationBar.tintColor = UIColor(hexString: tintColor) + } else { + navigationController?.navigationBar.tintColor = nil + } + } + + if newOptionsMap["hideToolbarBottom"] != nil, browserOptions?.hideToolbarBottom != newOptions.hideToolbarBottom { + navigationController?.isToolbarHidden = !newOptions.hideToolbarBottom + } + + if newOptionsMap["toolbarBottomBackgroundColor"] != nil, browserOptions?.toolbarBottomBackgroundColor != newOptions.toolbarBottomBackgroundColor { + if let bgColor = newOptions.toolbarBottomBackgroundColor, !bgColor.isEmpty { + navigationController?.toolbar.barTintColor = UIColor(hexString: bgColor) + } else { + navigationController?.toolbar.barTintColor = nil + } } - if newOptionsMap["toolbarBottom"] != nil && browserOptions?.toolbarBottom != newOptions.toolbarBottom { - self.containerWebView_BottomFullScreenConstraint.isActive = !newOptions.toolbarBottom - self.webView_BottomFullScreenConstraint.isActive = !newOptions.toolbarBottom - self.toolbarBottom.isHidden = !newOptions.toolbarBottom - self.toolbarBottom_TopToWebViewBottomConstraint.isActive = newOptions.toolbarBottom + if newOptionsMap["toolbarBottomTintColor"] != nil, browserOptions?.toolbarBottomTintColor != newOptions.toolbarBottomTintColor { + if let tintColor = newOptions.toolbarBottomTintColor, !tintColor.isEmpty { + navigationController?.toolbar.tintColor = UIColor(hexString: tintColor) + } else { + navigationController?.toolbar.tintColor = nil + } + } + + if newOptionsMap["toolbarTopTranslucent"] != nil, browserOptions?.toolbarTopTranslucent != newOptions.toolbarTopTranslucent { + navigationController?.navigationBar.isTranslucent = newOptions.toolbarTopTranslucent } - if newOptionsMap["toolbarBottomBackgroundColor"] != nil && browserOptions?.toolbarBottomBackgroundColor != newOptions.toolbarBottomBackgroundColor && newOptions.toolbarBottomBackgroundColor != "" { - self.toolbarBottom.backgroundColor = color(fromHexString: newOptions.toolbarBottomBackgroundColor) + if newOptionsMap["toolbarBottomTranslucent"] != nil, browserOptions?.toolbarBottomTranslucent != newOptions.toolbarBottomTranslucent { + navigationController?.toolbar.isTranslucent = newOptions.toolbarBottomTranslucent + } + + if newOptionsMap["closeButtonCaption"] != nil, browserOptions?.closeButtonCaption != newOptions.closeButtonCaption { + if let closeButtonCaption = newOptions.closeButtonCaption, !closeButtonCaption.isEmpty { + if let oldTitle = closeButton.title, !oldTitle.isEmpty { + closeButton.title = closeButtonCaption + } else { + closeButton.target = nil + closeButton.action = nil + closeButton = UIBarButtonItem(title: closeButtonCaption, style: .plain, target: self, action: #selector(close)) + } + } else { + setDefaultCloseButton() + } + } + + if newOptionsMap["closeButtonColor"] != nil, browserOptions?.closeButtonColor != newOptions.closeButtonColor { + if let tintColor = newOptions.closeButtonColor, !tintColor.isEmpty { + closeButton.tintColor = UIColor(hexString: tintColor) + } else { + closeButton.tintColor = nil + } } - if newOptionsMap["toolbarBottomTranslucent"] != nil && browserOptions?.toolbarBottomTranslucent != newOptions.toolbarBottomTranslucent { - self.toolbarBottom.isTranslucent = newOptions.toolbarBottomTranslucent + if newOptionsMap["presentationStyle"] != nil, browserOptions?.presentationStyle != newOptions.presentationStyle { + navigationController?.modalPresentationStyle = UIModalPresentationStyle(rawValue: newOptions.presentationStyle)! } - if newOptionsMap["closeButtonCaption"] != nil && browserOptions?.closeButtonCaption != newOptions.closeButtonCaption && newOptions.closeButtonCaption != "" { - closeButton.setTitle(newOptions.closeButtonCaption, for: .normal) + if newOptionsMap["transitionStyle"] != nil, browserOptions?.transitionStyle != newOptions.transitionStyle { + navigationController?.modalTransitionStyle = UIModalTransitionStyle(rawValue: newOptions.transitionStyle)! } - if newOptionsMap["closeButtonColor"] != nil && browserOptions?.closeButtonColor != newOptions.closeButtonColor && newOptions.closeButtonColor != "" { - closeButton.tintColor = color(fromHexString: newOptions.closeButtonColor) - } - - if newOptionsMap["presentationStyle"] != nil && browserOptions?.presentationStyle != newOptions.presentationStyle { - self.modalPresentationStyle = UIModalPresentationStyle(rawValue: newOptions.presentationStyle)! - } - - if newOptionsMap["transitionStyle"] != nil && browserOptions?.transitionStyle != newOptions.transitionStyle { - self.modalTransitionStyle = UIModalTransitionStyle(rawValue: newOptions.transitionStyle)! + if newOptionsMap["hideProgressBar"] != nil, browserOptions?.hideProgressBar != newOptions.hideProgressBar { + progressBar.isHidden = newOptions.hideProgressBar } self.browserOptions = newOptions @@ -484,19 +532,18 @@ public class InAppBrowserWebViewController: UIViewController, UIScrollViewDelega public func dispose() { webView.dispose() + webView = nil + view = nil if previousStatusBarStyle != -1 { UIApplication.shared.statusBarStyle = UIStatusBarStyle(rawValue: previousStatusBarStyle)! } transitioningDelegate = nil - urlField.delegate = nil - closeButton.removeTarget(self, action: #selector(self.close), for: .touchUpInside) - forwardButton.target = nil + searchBar.delegate = nil + closeButton.target = nil forwardButton.target = nil backButton.target = nil reloadButton.target = nil shareButton.target = nil - tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) - UIApplication.shared.delegate?.window??.makeKeyAndVisible() onExit() channel?.setMethodCallHandler(nil) channel = nil diff --git a/ios/Classes/InAppWebView/FlutterWebViewController.swift b/ios/Classes/InAppWebView/FlutterWebViewController.swift index 721fb011..12f7a326 100755 --- a/ios/Classes/InAppWebView/FlutterWebViewController.swift +++ b/ios/Classes/InAppWebView/FlutterWebViewController.swift @@ -34,15 +34,21 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { myView = UIView(frame: frame) myView!.clipsToBounds = true - let initialUrl = args["initialUrl"] as? String + let initialUrlRequest = args["initialUrlRequest"] as? [String: Any?] let initialFile = args["initialFile"] as? String let initialData = args["initialData"] as? [String: String] - let initialHeaders = args["initialHeaders"] as? [String: String] let initialOptions = args["initialOptions"] as! [String: Any?] let contextMenu = args["contextMenu"] as? [String: Any] let windowId = args["windowId"] as? Int64 let initialUserScripts = args["initialUserScripts"] as? [[String: Any]] - + + var userScripts: [UserScript] = [] + if let initialUserScripts = initialUserScripts { + for intialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: intialUserScript, windowId: windowId)!) + } + } + let options = InAppWebViewOptions() let _ = options.parse(options: initialOptions) let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(options: options) @@ -50,11 +56,15 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { webView = webViewTransport.webView webView!.frame = myView!.bounds - webView!.IABController = nil webView!.contextMenu = contextMenu webView!.channel = channel! + webView!.initialUserScripts = userScripts } else { - webView = InAppWebView(frame: myView!.bounds, configuration: preWebviewConfiguration, IABController: nil, contextMenu: contextMenu, channel: channel!) + webView = InAppWebView(frame: myView!.bounds, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + channel: channel!, + userScripts: userScripts) } methodCallDelegate = InAppWebViewMethodHandler(webView: webView!) @@ -66,9 +76,6 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { myView!.addSubview(webView!) webView!.options = options - if let userScripts = initialUserScripts { - webView!.appendUserScripts(userScripts: userScripts) - } webView!.prepare() if windowId == nil { @@ -90,7 +97,7 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { let configuration = self.webView!.configuration configuration.userContentController.add(contentRuleList!) - self.load(initialUrl: initialUrl, initialFile: initialFile, initialData: initialData, initialHeaders: initialHeaders) + self.load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) } return } catch { @@ -98,7 +105,7 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { } } } - load(initialUrl: initialUrl, initialFile: initialFile, initialData: initialData, initialHeaders: initialHeaders) + load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) } else if let wId = windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { webView!.load(webViewTransport.request) @@ -133,33 +140,32 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { return myView! } - public func load(initialUrl: String?, initialFile: String?, initialData: [String: String]?, initialHeaders: [String: String]?) { + public func load(initialUrlRequest: [String:Any?]?, initialFile: String?, initialData: [String: String]?) { if let initialFile = initialFile { do { - try webView?.loadFile(url: initialFile, headers: initialHeaders) + try webView?.loadFile(assetFilePath: initialFile) } catch let error as NSError { dump(error) } - return } - - if let initialData = initialData { + else if let initialData = initialData { let data = initialData["data"]! let mimeType = initialData["mimeType"]! let encoding = initialData["encoding"]! let baseUrl = initialData["baseUrl"]! webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl) } - else if let initialUrl = initialUrl, let url = URL(string: initialUrl) { + else if let initialUrlRequest = initialUrlRequest { + let urlRequest = URLRequest.init(fromPluginMap: initialUrlRequest) var allowingReadAccessToURL: URL? = nil - if let allowingReadAccessTo = webView?.options?.allowingReadAccessTo, url.scheme == "file" { + if let allowingReadAccessTo = webView?.options?.allowingReadAccessTo, let url = urlRequest.url, url.scheme == "file" { allowingReadAccessToURL = URL(string: allowingReadAccessTo) if allowingReadAccessToURL?.scheme != "file" { allowingReadAccessToURL = nil } } - webView?.loadUrl(url: url, headers: initialHeaders, allowingReadAccessTo: allowingReadAccessToURL) + webView?.loadUrl(urlRequest: urlRequest, allowingReadAccessTo: allowingReadAccessToURL) } } } diff --git a/ios/Classes/HeadlessInAppWebViewManager.swift b/ios/Classes/InAppWebView/HeadlessInAppWebViewManager.swift similarity index 100% rename from ios/Classes/HeadlessInAppWebViewManager.swift rename to ios/Classes/InAppWebView/HeadlessInAppWebViewManager.swift diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index 9f94cede..19b1184b 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -9,873 +9,28 @@ import Flutter import Foundation import WebKit -func currentTimeInMilliSeconds() -> Int64 { - let currentDate = Date() - let since1970 = currentDate.timeIntervalSince1970 - return Int64(since1970 * 1000) -} - -func convertToDictionary(text: String) -> [String: Any]? { - if let data = text.data(using: .utf8) { - do { - return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - } catch { - print(error.localizedDescription) - } - } - return nil -} - -func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { - let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) - if JSONSerialization.isValidJSONObject(value) { - let data = try? JSONSerialization.data(withJSONObject: value, options: options) - if data != nil { - if let string = String(data: data!, encoding: .utf8) { - return string - } - } - } - return "" -} - -let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" - -// https://github.com/tildeio/rsvp.js -let promisePolyfillJS = """ -if (window.Promise == null) { - !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.RSVP={})}(this,function(t){"use strict";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if("function"!=typeof r)throw new TypeError("Callback must be a function");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s= 0; i--) { - wkwebview_FindAllAsyncForElement( - element.childNodes[element.childNodes.length - 1 - i], - keyword - ); - } - } - } - } -} - -// the main entry point to start the search -function wkwebview_FindAllAsync(keyword) { - wkwebview_ClearMatches(); - wkwebview_FindAllAsyncForElement(document.body, keyword.toLowerCase()); - wkwebview_IsDoneCounting = true; - - var _windowId = window._flutter_inappwebview_windowId; - - window.webkit.messageHandlers["onFindResultReceived"].postMessage( - { - 'findResult': { - 'activeMatchOrdinal': wkwebview_CurrentHighlight, - 'numberOfMatches': wkwebview_SearchResultCount, - 'isDoneCounting': wkwebview_IsDoneCounting - }, - '_windowId': _windowId - } - ); -} - -// helper function, recursively removes the highlights in elements and their childs -function wkwebview_ClearMatchesForElement(element) { - if (element) { - if (element.nodeType == 1) { - if (element.getAttribute("class") == "wkwebview_Highlight") { - var text = element.removeChild(element.firstChild); - element.parentNode.insertBefore(text, element); - element.parentNode.removeChild(element); - return true; - } else { - var normalize = false; - for (var i = element.childNodes.length - 1; i >= 0; i--) { - if (wkwebview_ClearMatchesForElement(element.childNodes[i])) { - normalize = true; - } - } - if (normalize) { - element.normalize(); - } - } - } - } - return false; -} - -// the main entry point to remove the highlights -function wkwebview_ClearMatches() { - wkwebview_SearchResultCount = 0; - wkwebview_CurrentHighlight = 0; - wkwebview_ClearMatchesForElement(document.body); -} - -function wkwebview_FindNext(forward) { - if (wkwebview_SearchResultCount <= 0) return; - - var idx = wkwebview_CurrentHighlight + (forward ? +1 : -1); - idx = - idx < 0 - ? wkwebview_SearchResultCount - 1 - : idx >= wkwebview_SearchResultCount - ? 0 - : idx; - wkwebview_CurrentHighlight = idx; - - var scrollTo = document.getElementById("WKWEBVIEW_SEARCH_WORD_" + idx); - if (scrollTo) { - var highlights = document.getElementsByClassName("wkwebview_Highlight"); - for (var i = 0; i < highlights.length; i++) { - var span = highlights[i]; - span.style.backgroundColor = "#FFFF00"; - } - scrollTo.style.backgroundColor = "#FF9732"; - - scrollTo.scrollIntoView({ - behavior: "auto", - block: "center" - }); - - var _windowId = window._flutter_inappwebview_windowId; - - window.webkit.messageHandlers["onFindResultReceived"].postMessage( - { - 'findResult': { - 'activeMatchOrdinal': wkwebview_CurrentHighlight, - 'numberOfMatches': wkwebview_SearchResultCount, - 'isDoneCounting': wkwebview_IsDoneCounting - }, - '_windowId': _windowId - } - ); - } -} -""" - -let variableForOnLoadResourceJS = "_flutter_inappwebview_useOnLoadResource" -let enableVariableForOnLoadResourceJS = "window.\(variableForOnLoadResourceJS) = $PLACEHOLDER_VALUE;" - -let resourceObserverJS = """ -(function() { - var observer = new PerformanceObserver(function(list) { - list.getEntries().forEach(function(entry) { - if (window.\(variableForOnLoadResourceJS) == null || window.\(variableForOnLoadResourceJS) == true) { - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", entry); - } - }); - }); - observer.observe({entryTypes: ['resource']}); -})(); -""" - -let variableForShouldInterceptAjaxRequestJS = "_flutter_inappwebview_useShouldInterceptAjaxRequest" -let enableVariableForShouldInterceptAjaxRequestJS = "window.\(variableForShouldInterceptAjaxRequestJS) = $PLACEHOLDER_VALUE;" - -let interceptAjaxRequestsJS = """ -(function(ajax) { - var send = ajax.prototype.send; - var open = ajax.prototype.open; - var setRequestHeader = ajax.prototype.setRequestHeader; - ajax.prototype._flutter_inappwebview_url = null; - ajax.prototype._flutter_inappwebview_method = null; - ajax.prototype._flutter_inappwebview_isAsync = null; - ajax.prototype._flutter_inappwebview_user = null; - ajax.prototype._flutter_inappwebview_password = null; - ajax.prototype._flutter_inappwebview_password = null; - ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false; - ajax.prototype._flutter_inappwebview_request_headers = {}; - function convertRequestResponse(request, callback) { - if (request.response != null && request.responseType != null) { - switch (request.responseType) { - case 'arraybuffer': - callback(new Uint8Array(request.response)); - return; - case 'blob': - const reader = new FileReader(); - reader.addEventListener('loadend', function() { - callback(new Uint8Array(reader.result)); - }); - reader.readAsArrayBuffer(blob); - return; - case 'document': - callback(request.response.documentElement.outerHTML); - return; - case 'json': - callback(request.response); - return; - }; - } - callback(null); - }; - ajax.prototype.open = function(method, url, isAsync, user, password) { - isAsync = (isAsync != null) ? isAsync : true; - this._flutter_inappwebview_url = url; - this._flutter_inappwebview_method = method; - this._flutter_inappwebview_isAsync = isAsync; - this._flutter_inappwebview_user = user; - this._flutter_inappwebview_password = password; - this._flutter_inappwebview_request_headers = {}; - open.call(this, method, url, isAsync, user, password); - }; - ajax.prototype.setRequestHeader = function(header, value) { - this._flutter_inappwebview_request_headers[header] = value; - setRequestHeader.call(this, header, value); - }; - function handleEvent(e) { - var self = this; - if (window.\(variableForShouldInterceptAjaxRequestJS) == null || window.\(variableForShouldInterceptAjaxRequestJS) == true) { - var headers = this.getAllResponseHeaders(); - var responseHeaders = {}; - if (headers != null) { - var arr = headers.trim().split(/[\\r\\n]+/); - arr.forEach(function (line) { - var parts = line.split(': '); - var header = parts.shift(); - var value = parts.join(': '); - responseHeaders[header] = value; - }); - } - convertRequestResponse(this, function(response) { - var ajaxRequest = { - method: self._flutter_inappwebview_method, - url: self._flutter_inappwebview_url, - isAsync: self._flutter_inappwebview_isAsync, - user: self._flutter_inappwebview_user, - password: self._flutter_inappwebview_password, - withCredentials: self.withCredentials, - headers: self._flutter_inappwebview_request_headers, - readyState: self.readyState, - status: self.status, - responseURL: self.responseURL, - responseType: self.responseType, - response: response, - responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, - responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, - statusText: self.statusText, - responseHeaders, responseHeaders, - event: { - type: e.type, - loaded: e.loaded, - lengthComputable: e.lengthComputable, - total: e.total - } - }; - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxProgress', ajaxRequest).then(function(result) { - if (result != null) { - switch (result) { - case 0: - self.abort(); - return; - }; - } - }); - }); - } - }; - ajax.prototype.send = function(data) { - var self = this; - if (window.\(variableForShouldInterceptAjaxRequestJS) == null || window.\(variableForShouldInterceptAjaxRequestJS) == true) { - if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) { - this._flutter_inappwebview_already_onreadystatechange_wrapped = true; - var onreadystatechange = this.onreadystatechange; - this.onreadystatechange = function() { - if (window.\(variableForShouldInterceptAjaxRequestJS) == null || window.\(variableForShouldInterceptAjaxRequestJS) == true) { - var headers = this.getAllResponseHeaders(); - var responseHeaders = {}; - if (headers != null) { - var arr = headers.trim().split(/[\\r\\n]+/); - arr.forEach(function (line) { - var parts = line.split(': '); - var header = parts.shift(); - var value = parts.join(': '); - responseHeaders[header] = value; - }); - } - convertRequestResponse(this, function(response) { - var ajaxRequest = { - method: self._flutter_inappwebview_method, - url: self._flutter_inappwebview_url, - isAsync: self._flutter_inappwebview_isAsync, - user: self._flutter_inappwebview_user, - password: self._flutter_inappwebview_password, - withCredentials: self.withCredentials, - headers: self._flutter_inappwebview_request_headers, - readyState: self.readyState, - status: self.status, - responseURL: self.responseURL, - responseType: self.responseType, - response: response, - responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, - responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, - statusText: self.statusText, - responseHeaders: responseHeaders - }; - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) { - if (result != null) { - switch (result) { - case 0: - self.abort(); - return; - }; - } - if (onreadystatechange != null) { - onreadystatechange(); - } - }); - }); - } else if (onreadystatechange != null) { - onreadystatechange(); - } - }; - } - this.addEventListener('loadstart', handleEvent); - this.addEventListener('load', handleEvent); - this.addEventListener('loadend', handleEvent); - this.addEventListener('progress', handleEvent); - this.addEventListener('error', handleEvent); - this.addEventListener('abort', handleEvent); - this.addEventListener('timeout', handleEvent); - var ajaxRequest = { - data: data, - method: this._flutter_inappwebview_method, - url: this._flutter_inappwebview_url, - isAsync: this._flutter_inappwebview_isAsync, - user: this._flutter_inappwebview_user, - password: this._flutter_inappwebview_password, - withCredentials: this.withCredentials, - headers: this._flutter_inappwebview_request_headers, - responseType: this.responseType - }; - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { - if (result != null) { - switch (result.action) { - case 0: - self.abort(); - return; - }; - data = result.data; - self.withCredentials = result.withCredentials; - if (result.responseType != null) { - self.responseType = result.responseType; - }; - for (var header in result.headers) { - var value = result.headers[header]; - var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header]; - if (flutter_inappwebview_value == null) { - self._flutter_inappwebview_request_headers[header] = value; - } else { - self._flutter_inappwebview_request_headers[header] += ', ' + value; - } - setRequestHeader.call(self, header, value); - }; - if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) { - self.abort(); - self.open(result.method, result.url, result.isAsync, result.user, result.password); - return; - } - } - send.call(self, data); - }); - } else { - send.call(this, data); - } - }; -})(window.XMLHttpRequest); -""" - -let variableForShouldInterceptFetchRequestsJS = "_flutter_inappwebview_useShouldInterceptFetchRequest" -let enableVariableForShouldInterceptFetchRequestsJS = "window.\(variableForShouldInterceptFetchRequestsJS) = $PLACEHOLDER_VALUE;" - -let interceptFetchRequestsJS = """ -(function(fetch) { - if (fetch == null) { - return; - } - function convertHeadersToJson(headers) { - var headersObj = {}; - for (var header of headers.keys()) { - var value = headers.get(header); - headersObj[header] = value; - } - return headersObj; - } - function convertJsonToHeaders(headersJson) { - return new Headers(headersJson); - } - function convertBodyToArray(body) { - return new Response(body).arrayBuffer().then(function(arrayBuffer) { - var arr = Array.from(new Uint8Array(arrayBuffer)); - return arr; - }) - } - function convertArrayIntBodyToUint8Array(arrayIntBody) { - return new Uint8Array(arrayIntBody); - } - function convertCredentialsToJson(credentials) { - var credentialsObj = {}; - if (window.FederatedCredential != null && credentials instanceof FederatedCredential) { - credentialsObj.type = credentials.type; - credentialsObj.id = credentials.id; - credentialsObj.name = credentials.name; - credentialsObj.protocol = credentials.protocol; - credentialsObj.provider = credentials.provider; - credentialsObj.iconURL = credentials.iconURL; - } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) { - credentialsObj.type = credentials.type; - credentialsObj.id = credentials.id; - credentialsObj.name = credentials.name; - credentialsObj.password = credentials.password; - credentialsObj.iconURL = credentials.iconURL; - } else { - credentialsObj.type = 'default'; - credentialsObj.value = credentials; - } - } - function convertJsonToCredential(credentialsJson) { - var credentials; - if (window.FederatedCredential != null && credentialsJson.type === 'federated') { - credentials = new FederatedCredential({ - id: credentialsJson.id, - name: credentialsJson.name, - protocol: credentialsJson.protocol, - provider: credentialsJson.provider, - iconURL: credentialsJson.iconURL - }); - } else if (window.PasswordCredential != null && credentialsJson.type === 'password') { - credentials = new PasswordCredential({ - id: credentialsJson.id, - name: credentialsJson.name, - password: credentialsJson.password, - iconURL: credentialsJson.iconURL - }); - } else { - credentials = credentialsJson; - } - return credentials; - } - window.fetch = async function(resource, init) { - if (window.\(variableForShouldInterceptFetchRequestsJS) == null || window.\(variableForShouldInterceptFetchRequestsJS) == true) { - var fetchRequest = { - url: null, - method: null, - headers: null, - body: null, - mode: null, - credentials: null, - cache: null, - redirect: null, - referrer: null, - referrerPolicy: null, - integrity: null, - keepalive: null - }; - if (resource instanceof Request) { - fetchRequest.url = resource.url; - fetchRequest.method = resource.method; - fetchRequest.headers = resource.headers; - fetchRequest.body = resource.body; - fetchRequest.mode = resource.mode; - fetchRequest.credentials = resource.credentials; - fetchRequest.cache = resource.cache; - fetchRequest.redirect = resource.redirect; - fetchRequest.referrer = resource.referrer; - fetchRequest.referrerPolicy = resource.referrerPolicy; - fetchRequest.integrity = resource.integrity; - fetchRequest.keepalive = resource.keepalive; - } else { - fetchRequest.url = resource; - if (init != null) { - fetchRequest.method = init.method; - fetchRequest.headers = init.headers; - fetchRequest.body = init.body; - fetchRequest.mode = init.mode; - fetchRequest.credentials = init.credentials; - fetchRequest.cache = init.cache; - fetchRequest.redirect = init.redirect; - fetchRequest.referrer = init.referrer; - fetchRequest.referrerPolicy = init.referrerPolicy; - fetchRequest.integrity = init.integrity; - fetchRequest.keepalive = init.keepalive; - } - } - if (fetchRequest.headers instanceof Headers) { - fetchRequest.headers = convertHeadersToJson(fetchRequest.headers); - } - fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials); - return convertBodyToArray(fetchRequest.body).then(function(body) { - fetchRequest.body = body; - return window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) { - if (result != null) { - switch (result.action) { - case 0: - var controller = new AbortController(); - if (init != null) { - init.signal = controller.signal; - } else { - init = { - signal: controller.signal - }; - } - controller.abort(); - break; - } - resource = (result.url != null) ? result.url : resource; - if (init == null) { - init = {}; - } - if (result.method != null && result.method.length > 0) { - init.method = result.method; - } - if (result.headers != null && Object.keys(result.headers).length > 0) { - init.headers = convertJsonToHeaders(result.headers); - } - if (result.body != null && result.body.length > 0) { - init.body = convertArrayIntBodyToUint8Array(result.body); - } - if (result.mode != null && result.mode.length > 0) { - init.mode = result.mode; - } - if (result.credentials != null) { - init.credentials = convertJsonToCredential(result.credentials); - } - if (result.cache != null && result.cache.length > 0) { - init.cache = result.cache; - } - if (result.redirect != null && result.redirect.length > 0) { - init.redirect = result.redirect; - } - if (result.referrer != null && result.referrer.length > 0) { - init.referrer = result.referrer; - } - if (result.referrerPolicy != null && result.referrerPolicy.length > 0) { - init.referrerPolicy = result.referrerPolicy; - } - if (result.integrity != null && result.integrity.length > 0) { - init.integrity = result.integrity; - } - if (result.keepalive != null) { - init.keepalive = result.keepalive; - } - return fetch(resource, init); - } - return fetch(resource, init); - }); - }); - } else { - return fetch(resource, init); - } - }; -})(window.fetch); -""" - -/** - https://developer.android.com/reference/android/webkit/WebView.HitTestResult - */ -let findElementsAtPointJS = """ -window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { - var hitTestResultType = { - UNKNOWN_TYPE: 0, - PHONE_TYPE: 2, - GEO_TYPE: 3, - EMAIL_TYPE: 4, - IMAGE_TYPE: 5, - SRC_ANCHOR_TYPE: 7, - SRC_IMAGE_ANCHOR_TYPE: 8, - EDIT_TEXT_TYPE: 9 - }; - var element = document.elementFromPoint(x, y); - var data = { - type: 0, - extra: null - }; - while (element) { - if (element.tagName === 'IMG' && element.src) { - if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { - data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; - } else { - data.type = hitTestResultType.IMAGE_TYPE; - } - data.extra = element.src; - break; - } else if (element.tagName === 'A' && element.href) { - if (element.href.indexOf('mailto:') === 0) { - data.type = hitTestResultType.EMAIL_TYPE; - data.extra = element.href.replace('mailto:', ''); - } else if (element.href.indexOf('tel:') === 0) { - data.type = hitTestResultType.PHONE_TYPE; - data.extra = element.href.replace('tel:', ''); - } else if (element.href.indexOf('geo:') === 0) { - data.type = hitTestResultType.GEO_TYPE; - data.extra = element.href.replace('geo:', ''); - } else { - data.type = hitTestResultType.SRC_ANCHOR_TYPE; - data.extra = element.href; - } - break; - } else if ( - (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || - element.tagName === 'TEXTAREA') { - data.type = hitTestResultType.EDIT_TEXT_TYPE - } - element = element.parentNode; - } - return data; -} -""" - -let getSelectedTextJS = """ -(function(){ - var txt; - if (window.getSelection) { - txt = window.getSelection().toString(); - } else if (window.document.getSelection) { - txt = window.document.getSelection().toString(); - } else if (window.document.selection) { - txt = window.document.selection.createRange().text; - } - return txt; -})(); -""" - -let lastTouchedAnchorOrImageJS = """ -window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = null; -window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = null; -(function() { - document.addEventListener('touchstart', function(event) { - var target = event.target; - while (target) { - if (target.tagName === 'IMG') { - var img = target; - window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = { - src: img.src - }; - var parent = img.parentNode; - while (parent) { - if (parent.tagName === 'A') { - window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { - title: parent.textContent, - url: parent.href, - src: img.src - }; - break; - } - parent = parent.parentNode; - } - return; - } else if (target.tagName === 'A') { - var link = target; - var images = link.getElementsByTagName('img'); - var img = (images.length > 0) ? images[0] : null; - var imgSrc = (img != null) ? img.src : null; - window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = (img != null) ? {src: img.src} : window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched; - window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { - title: link.textContent, - url: link.href, - src: imgSrc - }; - return; - } - target = target.parentNode; - } - }); -})(); -""" - -let originalViewPortMetaTagContentJS = """ -window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; -(function() { - var metaTagNodes = document.head.getElementsByTagName('meta'); - for (var i = 0; i < metaTagNodes.length; i++) { - var metaTagNode = metaTagNodes[i]; - if (metaTagNode.name === "viewport") { - window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; - } - } -})(); -""" - -let onWindowFocusEventJS = """ -(function(){ - window.addEventListener('focus', function(e) { - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); - }); -})(); -""" - -let onWindowBlurEventJS = """ -(function(){ - window.addEventListener('blur', function(e) { - window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); - }); -})(); -""" - -let callAsyncJavaScriptBelowIOS14WrapperJS = """ -(function(obj) { - (async function($FUNCTION_ARGUMENT_NAMES) { - $FUNCTION_BODY - })($FUNCTION_ARGUMENT_VALUES).then(function(value) { - window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '$RESULT_UUID'}); - }).catch(function(error) { - window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error, 'resultUuid': '$RESULT_UUID'}); - }); - return null; -})($FUNCTION_ARGUMENTS_OBJ); -""" - -var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] - -public class WebViewTransport: NSObject { - var webView: InAppWebView - var request: URLRequest - - init(webView: InAppWebView, request: URLRequest) { - self.webView = webView - self.request = request - } -} - public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate { var windowId: Int64? - var IABController: InAppBrowserWebViewController? + var inAppBrowserDelegate: InAppBrowserDelegate? var channel: FlutterMethodChannel? var options: InAppWebViewOptions? var currentURL: URL? - var x509CertificateData: Data? - static var sslCertificateMap: [String: Data] = [:] // [URL host name : x509Certificate Data] - var startPageTime: Int64 = 0 + static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] static var credentialsProposed: [URLCredential] = [] var lastScrollX: CGFloat = 0 var lastScrollY: CGFloat = 0 var isPausedTimers = false var isPausedTimersCompletionHandler: (() -> Void)? - // This flag is used to block the "shouldOverrideUrlLoading" event when the WKWebView is loading the first time, - // in order to have the same behavior as Android - var activateShouldOverrideUrlLoading = false + var contextMenu: [String: Any]? - var userScripts: [WKUserScript] = [] + var initialUserScripts: [UserScript] = [] // https://github.com/mozilla-mobile/firefox-ios/blob/50531a7e9e4d459fb11d4fcb7d4322e08103501f/Client/Frontend/Browser/ContextMenuHelper.swift fileprivate var nativeHighlightLongPressRecognizer: UILongPressGestureRecognizer? - var longPressRecognizer: UILongPressGestureRecognizer? + fileprivate var nativeLoupeGesture: UILongPressGestureRecognizer? + var longPressRecognizer: UILongPressGestureRecognizer! + var recognizerForDisablingContextMenuOnLinks: UILongPressGestureRecognizer! var lastLongPressTouchPoint: CGPoint? var lastTouchPoint: CGPoint? @@ -890,21 +45,23 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi static var windowWebViews: [Int64:WebViewTransport] = [:] static var windowAutoincrementId: Int64 = 0; - var userScriptsContentWorlds: [String] = ["page"] + var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:] - var callAsyncJavaScriptBelowIOS14Results: [String:FlutterResult] = [:] - - init(frame: CGRect, configuration: WKWebViewConfiguration, IABController: InAppBrowserWebViewController?, contextMenu: [String: Any]?, channel: FlutterMethodChannel?) { + init(frame: CGRect, configuration: WKWebViewConfiguration, contextMenu: [String: Any]?, channel: FlutterMethodChannel?, userScripts: [UserScript] = []) { super.init(frame: frame, configuration: configuration) self.channel = channel self.contextMenu = contextMenu - self.IABController = IABController + self.initialUserScripts = userScripts uiDelegate = self navigationDelegate = self scrollView.delegate = self - self.longPressRecognizer = UILongPressGestureRecognizer() - self.longPressRecognizer!.delegate = self - self.longPressRecognizer!.addTarget(self, action: #selector(longPressGestureDetected)) + longPressRecognizer = UILongPressGestureRecognizer() + longPressRecognizer.delegate = self + longPressRecognizer.addTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks = UILongPressGestureRecognizer() + recognizerForDisablingContextMenuOnLinks.delegate = self + recognizerForDisablingContextMenuOnLinks.addTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks?.minimumPressDuration = 0.45 } override public var frame: CGRect { @@ -951,6 +108,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi // WebKit installs gesture handlers async. If `replaceWebViewLongPress` is called after a wkwebview in most cases a small delay is sufficient // See also https://bugs.webkit.org/show_bug.cgi?id=193366 nativeHighlightLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_highlightLongPressRecognized:") + nativeLoupeGesture = gestureRecognizerWithDescriptionFragment("action=loupeGesture:") if let nativeLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_longPressRecognized:") { nativeLongPressRecognizer.removeTarget(nil, action: nil) @@ -960,7 +118,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi private func gestureRecognizerWithDescriptionFragment(_ descriptionFragment: String) -> UILongPressGestureRecognizer? { let result = self.scrollView.subviews.compactMap({ $0.gestureRecognizers }).joined().first(where: { - (($0 as? UILongPressGestureRecognizer) != nil) && $0.description.contains(descriptionFragment) + return (($0 as? UILongPressGestureRecognizer) != nil) && $0.description.contains(descriptionFragment) }) return result as? UILongPressGestureRecognizer } @@ -973,32 +131,49 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi guard sender.state == .began else { return } - - // To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView - // `_highlightLongPressRecognizer`. This preserves the original behavior as seen here: - // https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614 - if let nativeHighlightLongPressRecognizer = self.nativeHighlightLongPressRecognizer, - nativeHighlightLongPressRecognizer.isEnabled { - nativeHighlightLongPressRecognizer.isEnabled = false - nativeHighlightLongPressRecognizer.isEnabled = true + + if sender == recognizerForDisablingContextMenuOnLinks, + let options = options, !options.disableLongPressContextMenuOnLinks { + return } + if sender == longPressRecognizer { + // To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView + // `_highlightLongPressRecognizer`. This preserves the original behavior as seen here: + // https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614 + if let nativeHighlightLongPressRecognizer = nativeHighlightLongPressRecognizer, + nativeHighlightLongPressRecognizer.isEnabled { + nativeHighlightLongPressRecognizer.isEnabled = false + nativeHighlightLongPressRecognizer.isEnabled = true + } + } + //Finding actual touch location in webView var touchLocation = sender.location(in: self) - touchLocation.x -= self.scrollView.contentInset.left - touchLocation.y -= self.scrollView.contentInset.top - touchLocation.x /= self.scrollView.zoomScale - touchLocation.y /= self.scrollView.zoomScale - + touchLocation.x -= scrollView.contentInset.left + touchLocation.y -= scrollView.contentInset.top + touchLocation.x /= scrollView.zoomScale + touchLocation.y /= scrollView.zoomScale + lastLongPressTouchPoint = touchLocation - self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler: {(value, error) in + evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler: {(value, error) in if error != nil { print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")") - } else if let value = value { - self.onLongPressHitTestResult(hitTestResult: value as! [String: Any?]) - } else { - self.onLongPressHitTestResult(hitTestResult: [:]) + } else if let value = value as? [String: Any?] { + let hitTestResult = HitTestResult.fromMap(map: value)! + self.nativeLoupeGesture = self.gestureRecognizerWithDescriptionFragment("action=loupeGesture:") + + if sender == self.recognizerForDisablingContextMenuOnLinks, + hitTestResult.type.rawValue > HitTestResultType.unknownType.rawValue, + hitTestResult.type.rawValue < HitTestResultType.editTextType.rawValue { + self.nativeLoupeGesture?.isEnabled = false + self.nativeLoupeGesture?.isEnabled = true + } else { + self.onLongPressHitTestResult(hitTestResult: hitTestResult) + } + } else if sender == self.longPressRecognizer { + self.onLongPressHitTestResult(hitTestResult: HitTestResult(type: .unknownType, extra: nil)) } }) } @@ -1078,7 +253,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } public func prepare() { - self.scrollView.addGestureRecognizer(self.longPressRecognizer!) + self.scrollView.addGestureRecognizer(self.longPressRecognizer) + self.scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks) addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), @@ -1166,7 +342,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi scrollView.isScrollEnabled = !(options.disableVerticalScroll && options.disableHorizontalScroll) scrollView.isDirectionalLockEnabled = options.isDirectionalLockEnabled - scrollView.decelerationRate = InAppWebView.getDecelerationRate(type: options.decelerationRate) + scrollView.decelerationRate = Util.getDecelerationRate(type: options.decelerationRate) scrollView.alwaysBounceVertical = options.alwaysBounceVertical scrollView.alwaysBounceHorizontal = options.alwaysBounceHorizontal scrollView.scrollsToTop = options.scrollsToTop @@ -1190,7 +366,9 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi prepareAndAddUserScripts() if windowId != nil { - // the new created window webview has the same WKWebViewConfiguration variable reference + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. return } @@ -1224,357 +402,45 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return } - addWindowIdUserScript() if windowId != nil { - // the new created window webview has the same WKWebViewConfiguration variable reference + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. return } configuration.userContentController = WKUserContentController() - addPluginUserScripts() - addAllUserScripts() - } - - func addWindowIdUserScript() -> Void { - let userScriptWindowId = WKUserScript(source: "window._flutter_inappwebview_windowId = \(windowId == nil ? "null" : String(windowId!));" , injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(userScriptWindowId) - } - - func getAllPluginUserScriptMergedJS() -> String { - var allPluginUserScriptMergedJS = promisePolyfillJS + "\n" + - javaScriptBridgeJS + "\n" + - consoleLogJS + "\n" + - printJS + "\n" + configuration.userContentController.initialize() + configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT) if let options = options { if options.useShouldInterceptAjaxRequest { - allPluginUserScriptMergedJS += interceptAjaxRequestsJS + "\n" + configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) } - if options.useShouldInterceptFetchRequest { - allPluginUserScriptMergedJS += interceptFetchRequestsJS + "\n" + configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) } - } - return allPluginUserScriptMergedJS - } - - @available(iOS 14.0, *) - func addSharedPluginUserScriptsInContentWorld(contentWorldName: String) -> Void { - let contentWorld = getContentWorld(name: contentWorldName) - - let promisePolyfillJSScript = WKUserScript(source: promisePolyfillJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(promisePolyfillJSScript) - - let javaScriptBridgeJSScript = WKUserScript(source: javaScriptBridgeJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(javaScriptBridgeJSScript) - configuration.userContentController.removeScriptMessageHandler(forName: "callHandler", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "callHandler") - - let consoleLogJSScript = WKUserScript(source: consoleLogJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(consoleLogJSScript) - configuration.userContentController.removeScriptMessageHandler(forName: "consoleLog", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "consoleLog") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleDebug", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "consoleDebug") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleError", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "consoleError") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleInfo", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "consoleInfo") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleWarn", contentWorld: contentWorld) - configuration.userContentController.add(self, contentWorld: contentWorld, name: "consoleWarn") - - if let options = options { - if options.useShouldInterceptAjaxRequest { - let interceptAjaxRequestsJSScript = WKUserScript(source: interceptAjaxRequestsJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(interceptAjaxRequestsJSScript) - } - - if options.useShouldInterceptFetchRequest { - let interceptFetchRequestsJSScript = WKUserScript(source: interceptFetchRequestsJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(interceptFetchRequestsJSScript) - } - } - - let printJSScript = WKUserScript(source: printJS, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: contentWorld) - configuration.userContentController.addUserScript(printJSScript) - } - - func addSharedPluginUserScriptsInContentWorlds() -> Void { - if #available(iOS 14.0, *) { - for contentWorldName in userScriptsContentWorlds { - addSharedPluginUserScriptsInContentWorld(contentWorldName: contentWorldName) - } - } else { - let promisePolyfillJSScript = WKUserScript( - source: promisePolyfillJS, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) - configuration.userContentController.addUserScript(promisePolyfillJSScript) - - let javaScriptBridgeJSScript = WKUserScript( - source: javaScriptBridgeJS, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) - configuration.userContentController.addUserScript(javaScriptBridgeJSScript) - configuration.userContentController.removeScriptMessageHandler(forName: "callHandler") - configuration.userContentController.add(self, name: "callHandler") - - let consoleLogJSScript = WKUserScript( - source: consoleLogJS, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) - configuration.userContentController.addUserScript(consoleLogJSScript) - configuration.userContentController.removeScriptMessageHandler(forName: "consoleLog") - configuration.userContentController.add(self, name: "consoleLog") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleDebug") - configuration.userContentController.add(self, name: "consoleDebug") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleError") - configuration.userContentController.add(self, name: "consoleError") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleInfo") - configuration.userContentController.add(self, name: "consoleInfo") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleWarn") - configuration.userContentController.add(self, name: "consoleWarn") - - if let options = options { - if options.useShouldInterceptAjaxRequest { - let interceptAjaxRequestsJSScript = WKUserScript( - source: interceptAjaxRequestsJS, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) - configuration.userContentController.addUserScript(interceptAjaxRequestsJSScript) - } - - if options.useShouldInterceptFetchRequest { - let interceptFetchRequestsJSScript = WKUserScript( - source: interceptFetchRequestsJS, - injectionTime: .atDocumentStart, - forMainFrameOnly: false) - configuration.userContentController.addUserScript(interceptFetchRequestsJSScript) - } - } - - let printJSScript = WKUserScript(source: printJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(printJSScript) - - configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") - configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") - } - } - - func addPluginUserScripts() -> Void { - if let applePayAPIEnabled = options?.applePayAPIEnabled, applePayAPIEnabled { - return - } - addSharedPluginUserScriptsInContentWorlds() - - let findElementsAtPointJSScript = WKUserScript(source: findElementsAtPointJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(findElementsAtPointJSScript) - - let lastTouchedAnchorOrImageJSScript = WKUserScript(source: lastTouchedAnchorOrImageJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(lastTouchedAnchorOrImageJSScript) - - let findTextHighlightJSScript = WKUserScript(source: findTextHighlightJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(findTextHighlightJSScript) - configuration.userContentController.removeScriptMessageHandler(forName: "onFindResultReceived") - configuration.userContentController.add(self, name: "onFindResultReceived") - - let onWindowFocusEventJSScript = WKUserScript(source: onWindowFocusEventJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(onWindowFocusEventJSScript) - - let onWindowBlurEventJSScript = WKUserScript(source: onWindowBlurEventJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(onWindowBlurEventJSScript) - - if let options = options { - let originalViewPortMetaTagContentJSScript = WKUserScript(source: originalViewPortMetaTagContentJS, injectionTime: .atDocumentEnd, forMainFrameOnly: true) - configuration.userContentController.addUserScript(originalViewPortMetaTagContentJSScript) - - if !options.supportZoom { - let jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); document.getElementsByTagName('head')[0].appendChild(meta);" - let userScript = WKUserScript(source: jscript, injectionTime: .atDocumentEnd, forMainFrameOnly: true) - configuration.userContentController.addUserScript(userScript) - } else if options.enableViewportScale { - let jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);" - let userScript = WKUserScript(source: jscript, injectionTime: .atDocumentEnd, forMainFrameOnly: true) - configuration.userContentController.addUserScript(userScript) - } - if options.useOnLoadResource { - let resourceObserverJSScript = WKUserScript(source: resourceObserverJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(resourceObserverJSScript) + configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } + if !options.supportZoom { + configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } else if options.enableViewportScale { + configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) } } - } - - func addAllUserScripts() -> Void { - for userScript in userScripts { - configuration.userContentController.addUserScript(userScript) - } - } - - public func addUserScript(wkUserScript: WKUserScript) -> Void { - if let applePayAPIEnabled = options?.applePayAPIEnabled, applePayAPIEnabled { - return - } - - userScripts.append(wkUserScript) - configuration.userContentController.addUserScript(wkUserScript) - } - - public func appendUserScript(userScript: [String: Any]) -> Void { - var wkUserScript: WKUserScript? - let contentWorldName = userScript["contentWorld"] as? String - if contentWorldName != nil, !userScriptsContentWorlds.contains(contentWorldName!) { - userScriptsContentWorlds.append(contentWorldName!) - } - if #available(iOS 14.0, *), let contentWorldName = contentWorldName { - wkUserScript = WKUserScript(source: userScript["source"] as! String, - injectionTime: WKUserScriptInjectionTime.init(rawValue: userScript["injectionTime"] as! Int) ?? .atDocumentStart, - forMainFrameOnly: userScript["iosForMainFrameOnly"] as! Bool, - in: getContentWorld(name: contentWorldName)) - } else { - wkUserScript = WKUserScript(source: userScript["source"] as! String, - injectionTime: WKUserScriptInjectionTime.init(rawValue: userScript["injectionTime"] as! Int) ?? .atDocumentStart, - forMainFrameOnly: userScript["iosForMainFrameOnly"] as! Bool) - } - userScripts.append(wkUserScript!) - } - - public func appendUserScripts(wkUserScripts: [WKUserScript]) -> Void { - for wkUserScript in wkUserScripts { - userScripts.append(wkUserScript) - } - } - - public func appendUserScripts(userScripts: [[String: Any]]) -> Void { - for userScript in userScripts { - appendUserScript(userScript: userScript) - } - } - - public func removeUserScript(at index: Int) -> Void { - userScripts.remove(at: index) - // there isn't a way to remove a specific user script using WKUserContentController, - // so we remove all the user scripts and, then, we add them again without the one that has been removed - configuration.userContentController.removeAllUserScripts() - - if let applePayAPIEnabled = options?.applePayAPIEnabled, applePayAPIEnabled { - return - } - - addWindowIdUserScript() - addPluginUserScripts() - addAllUserScripts() - } - - public func removeAllUserScripts() -> Void { - userScripts.removeAll() - configuration.userContentController.removeAllUserScripts() - - if let applePayAPIEnabled = options?.applePayAPIEnabled, applePayAPIEnabled { - return - } - - // add all the necessary base WKUserScripts of this plugin again - addWindowIdUserScript() - addPluginUserScripts() - } - - @available(iOS 14.0, *) - func getContentWorld(name: String) -> WKContentWorld { - switch name { - case "defaultClient": - return WKContentWorld.defaultClient - case "page": - return WKContentWorld.page - default: - return WKContentWorld.world(name: name) - } - } - - @available(iOS 10.0, *) - static public func getDataDetectorType(type: String) -> WKDataDetectorTypes { - switch type { - case "NONE": - return WKDataDetectorTypes.init(rawValue: 0) - case "PHONE_NUMBER": - return .phoneNumber - case "LINK": - return .link - case "ADDRESS": - return .address - case "CALENDAR_EVENT": - return .calendarEvent - case "TRACKING_NUMBER": - return .trackingNumber - case "FLIGHT_NUMBER": - return .flightNumber - case "LOOKUP_SUGGESTION": - return .lookupSuggestion - case "SPOTLIGHT_SUGGESTION": - return .spotlightSuggestion - case "ALL": - return .all - default: - return WKDataDetectorTypes.init(rawValue: 0) - } - } - - @available(iOS 10.0, *) - static public func getDataDetectorTypeString(type: WKDataDetectorTypes) -> [String] { - var dataDetectorTypeString: [String] = [] - if type.contains(.all) { - dataDetectorTypeString.append("ALL") - } else { - if type.contains(.phoneNumber) { - dataDetectorTypeString.append("PHONE_NUMBER") - } - if type.contains(.link) { - dataDetectorTypeString.append("LINK") - } - if type.contains(.address) { - dataDetectorTypeString.append("ADDRESS") - } - if type.contains(.calendarEvent) { - dataDetectorTypeString.append("CALENDAR_EVENT") - } - if type.contains(.trackingNumber) { - dataDetectorTypeString.append("TRACKING_NUMBER") - } - if type.contains(.flightNumber) { - dataDetectorTypeString.append("FLIGHT_NUMBER") - } - if type.contains(.lookupSuggestion) { - dataDetectorTypeString.append("LOOKUP_SUGGESTION") - } - if type.contains(.spotlightSuggestion) { - dataDetectorTypeString.append("SPOTLIGHT_SUGGESTION") - } - } - if dataDetectorTypeString.count == 0 { - dataDetectorTypeString = ["NONE"] - } - return dataDetectorTypeString - } - - static public func getDecelerationRate(type: String) -> UIScrollView.DecelerationRate { - switch type { - case "NORMAL": - return .normal - case "FAST": - return .fast - default: - return .normal - } - } - - static public func getDecelerationRateString(type: UIScrollView.DecelerationRate) -> String { - switch type { - case .normal: - return "NORMAL" - case .fast: - return "FAST" - default: - return "NORMAL" - } + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.addUserOnlyScripts(initialUserScripts) + configuration.userContentController.sync(scriptMessageHandler: self) } public static func preWKWebViewConfiguration(options: InAppWebViewOptions?) -> WKWebViewConfiguration { @@ -1608,7 +474,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0) for type in options.dataDetectorTypes { - let dataDetectorType = InAppWebView.getDataDetectorType(type: type) + let dataDetectorType = Util.getDataDetectorType(type: type) dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue) } configuration.dataDetectorTypes = dataDetectorTypes @@ -1654,27 +520,23 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi contextMenuIsShowing = true - var arguments: [String: Any?] = [ - "hitTestResult": nil - ] if let lastLongPressTouhLocation = lastLongPressTouchPoint { if configuration.preferences.javaScriptEnabled { self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastLongPressTouhLocation.x),\(lastLongPressTouhLocation.y))", completionHandler: {(value, error) in if error != nil { print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")") - } else if let value = value { - let hitTestResult = value as! [String: Any?] - arguments["hitTestResult"] = hitTestResult - self.channel?.invokeMethod("onCreateContextMenu", arguments: arguments) + } else if var value = value as? [String: Any?] { + value["type"] = value["type"] as? Int + self.channel?.invokeMethod("onCreateContextMenu", arguments: value) } else { - self.channel?.invokeMethod("onCreateContextMenu", arguments: arguments) + self.channel?.invokeMethod("onCreateContextMenu", arguments: [:]) } }) } else { - channel?.invokeMethod("onCreateContextMenu", arguments: arguments) + channel?.invokeMethod("onCreateContextMenu", arguments: [:]) } } else { - channel?.invokeMethod("onCreateContextMenu", arguments: arguments) + channel?.invokeMethod("onCreateContextMenu", arguments: [:]) } } @@ -1692,18 +554,38 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == #keyPath(WKWebView.estimatedProgress) { + initializeWindowIdJS() let progress = Int(estimatedProgress * 100) onProgressChanged(progress: progress) + inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress) } else if keyPath == #keyPath(WKWebView.url) && change?[NSKeyValueChangeKey.newKey] is URL { + initializeWindowIdJS() let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL onUpdateVisitedHistory(url: newUrl?.absoluteString) + inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl) } else if keyPath == #keyPath(WKWebView.title) && change?[NSKeyValueChangeKey.newKey] is String { let newTitle = change?[NSKeyValueChangeKey.newKey] as? String onTitleChanged(title: newTitle) + inAppBrowserDelegate?.didChangeTitle(title: newTitle) } replaceGestureHandlerIfNeeded() } + public func initializeWindowIdJS() { + if let windowId = windowId { + if #available(iOS 14.0, *) { + let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId) + for contentWorld in contentWorlds { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source, contentWorld: contentWorld) + } + } else { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source) + } + } + } + public func goBackOrForward(steps: Int) { if canGoBackOrForward(steps: steps) { if (steps > 0) { @@ -1838,41 +720,25 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi }) } - public func loadUrl(url: URL, headers: [String: String]?, allowingReadAccessTo: URL?) { + public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) { + let url = urlRequest.url! + currentURL = url + if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" { loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo) } else { - var request = URLRequest(url: url) - currentURL = url - if headers != nil { - if let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest { - for (key, value) in headers! { - mutableRequest.setValue(value, forHTTPHeaderField: key) - } - request = mutableRequest as URLRequest - } - } - load(request) + load(urlRequest) } } - public func postUrl(url: URL, postData: Data, completionHandler: @escaping () -> Void) { + public func postUrl(url: URL, postData: Data) { var request = URLRequest(url: url) currentURL = url + + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = postData - - let task = URLSession.shared.dataTask(with: request) { (data : Data?, response : URLResponse?, error : Error?) in - var returnString = "" - if data != nil { - returnString = String(data: data!, encoding: .utf8) ?? "" - } - DispatchQueue.main.async(execute: {() -> Void in - self.loadHTMLString(returnString, baseURL: url) - completionHandler() - }) - } - task.resume() + load(request) } public func loadData(data: String, mimeType: String, encoding: String, baseUrl: String) { @@ -1885,13 +751,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } - public func loadFile(url: String, headers: [String: String]?) throws { - let key = SwiftFlutterPlugin.instance!.registrar!.lookupKey(forAsset: url) - let assetURL = Bundle.main.url(forResource: key, withExtension: nil) - if assetURL == nil { - throw NSError(domain: url + " asset file cannot be found!", code: 0) - } - loadUrl(url: assetURL!, headers: headers, allowingReadAccessTo: nil) + public func loadFile(assetFilePath: String) throws { + let assetURL = try Util.getUrlAsset(assetFilePath: assetFilePath) + let urlRequest = URLRequest(url: assetURL) + loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil) } func setOptions(newOptions: InAppWebViewOptions, newOptionsMap: [String: Any]) { @@ -1963,55 +826,56 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } if newOptionsMap["enableViewportScale"] != nil && options?.enableViewportScale != newOptions.enableViewportScale { - var jscript = "" - if (newOptions.enableViewportScale) { - jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);" + if !newOptions.enableViewportScale { + if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE) + } } else { - jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); document.getElementsByTagName('head')[0].appendChild(meta);" + evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE) + configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) } - evaluateJavaScript(jscript, completionHandler: nil) } if newOptionsMap["supportZoom"] != nil && options?.supportZoom != newOptions.supportZoom { - var jscript = "" - if (newOptions.supportZoom) { - jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); document.getElementsByTagName('head')[0].appendChild(meta);" - } else { - jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); document.getElementsByTagName('head')[0].appendChild(meta);" - } - evaluateJavaScript(jscript, completionHandler: nil) - } - - if newOptionsMap["useOnLoadResource"] != nil && options?.useOnLoadResource != newOptions.useOnLoadResource && newOptions.useOnLoadResource { - let placeholderValue = newOptions.useOnLoadResource ? "true" : "false" - let source = enableVariableForOnLoadResourceJS.replacingOccurrences(of: "$PLACEHOLDER_VALUE", with: placeholderValue) - evaluateJavaScript(source, completionHandler: nil) - } - - if newOptionsMap["useShouldInterceptAjaxRequest"] != nil && options?.useShouldInterceptAjaxRequest != newOptions.useShouldInterceptAjaxRequest && newOptions.useShouldInterceptAjaxRequest { - let placeholderValue = newOptions.useShouldInterceptAjaxRequest ? "true" : "false" - let source = enableVariableForShouldInterceptAjaxRequestJS.replacingOccurrences(of: "$PLACEHOLDER_VALUE", with: placeholderValue) - if #available(iOS 14.0, *) { - for contentWorldName in userScriptsContentWorlds { - let contentWorld = getContentWorld(name: contentWorldName) - evaluateJavaScript(source, frame: nil, contentWorld: contentWorld, completionHandler: nil) + if newOptions.supportZoom { + if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE) } } else { - evaluateJavaScript(source, completionHandler: nil) + evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE) + configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) } } - if newOptionsMap["useShouldInterceptFetchRequest"] != nil && options?.useShouldInterceptFetchRequest != newOptions.useShouldInterceptFetchRequest && newOptions.useShouldInterceptFetchRequest { - let placeholderValue = newOptions.useShouldInterceptFetchRequest ? "true" : "false" - - let source = enableVariableForShouldInterceptAjaxRequestJS.replacingOccurrences(of: "$PLACEHOLDER_VALUE", with: placeholderValue) - if #available(iOS 14.0, *) { - for contentWorldName in userScriptsContentWorlds { - let contentWorld = getContentWorld(name: contentWorldName) - evaluateJavaScript(source, frame: nil, contentWorld: contentWorld, completionHandler: nil) - } + if newOptionsMap["useOnLoadResource"] != nil && options?.useOnLoadResource != newOptions.useOnLoadResource { + if let applePayAPIEnabled = options?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + enable: newOptions.useOnLoadResource, + pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) } else { - evaluateJavaScript(source, completionHandler: nil) + newOptions.useOnLoadResource = false + } + } + + if newOptionsMap["useShouldInterceptAjaxRequest"] != nil && options?.useShouldInterceptAjaxRequest != newOptions.useShouldInterceptAjaxRequest { + if let applePayAPIEnabled = options?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + enable: newOptions.useShouldInterceptAjaxRequest, + pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newOptions.useShouldInterceptFetchRequest = false + } + } + + if newOptionsMap["useShouldInterceptFetchRequest"] != nil && options?.useShouldInterceptFetchRequest != newOptions.useShouldInterceptFetchRequest { + if let applePayAPIEnabled = options?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + enable: newOptions.useShouldInterceptFetchRequest, + pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newOptions.useShouldInterceptFetchRequest = false } } @@ -2056,7 +920,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi if newOptionsMap["dataDetectorTypes"] != nil && options?.dataDetectorTypes != newOptions.dataDetectorTypes { var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0) for type in newOptions.dataDetectorTypes { - let dataDetectorType = InAppWebView.getDataDetectorType(type: type) + let dataDetectorType = Util.getDataDetectorType(type: type) dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue) } configuration.dataDetectorTypes = dataDetectorTypes @@ -2094,7 +958,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } if newOptionsMap["decelerationRate"] != nil && options?.decelerationRate != newOptions.decelerationRate { - scrollView.decelerationRate = InAppWebView.getDecelerationRate(type: newOptions.decelerationRate) + scrollView.decelerationRate = Util.getDecelerationRate(type: newOptions.decelerationRate) } if newOptionsMap["alwaysBounceVertical"] != nil && options?.alwaysBounceVertical != newOptions.alwaysBounceVertical { scrollView.alwaysBounceVertical = newOptions.alwaysBounceVertical @@ -2200,6 +1064,36 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return self.options!.getRealOptions(obj: self) } + public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) { + evaluateJavascript(source: flagVariable) { (alreadyLoaded) in + if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded { + let enableSource = "\(flagVariable) = \(enable);" + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil) + } + } else { + self.evaluateJavaScript(enableSource, completionHandler: nil) + } + if !enable { + self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!) + } + } + else if enable { + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + } else { + self.evaluateJavaScript(pluginScript.source, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + self.configuration.userContentController.sync(scriptMessageHandler: self) + } + } + } + public func clearCache() { if #available(iOS 9.0, *) { //let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]) @@ -2218,7 +1112,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } - public func injectDeferredObject(source: String, contentWorldName: String?, withWrapper jsWrapper: String?, result: FlutterResult?) { + public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { var jsToInject = source if let wrapper = jsWrapper { let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) @@ -2227,50 +1121,59 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi jsToInject = String(format: wrapper, sourceString!) } - if #available(iOS 14.0, *), let contentWorldName = contentWorldName { - if !userScriptsContentWorlds.contains(contentWorldName) { - userScriptsContentWorlds.append(contentWorldName) - addSharedPluginUserScriptsInContentWorld(contentWorldName: contentWorldName) - // Add only the first time all the plugin user scripts needed. - // In the next page load, it will use the WKUserScripts loaded - jsToInject = getAllPluginUserScriptMergedJS() + "\n" + jsToInject + evaluateJavaScript(jsToInject) { (value, error) in + guard let completionHandler = completionHandler else { + return } - let contentWorld = getContentWorld(name: contentWorldName) - evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in - guard let result = result else { - return - } - - switch (evalResult) { - case .success(let value): - result(value) - return - case .failure(let error): - let userInfo = (error as NSError).userInfo - self.onConsoleMessage(message: userInfo["WKJavaScriptExceptionMessage"] as? String ?? "", messageLevel: 3) - break - } - - result(nil) + + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) } - } else { - evaluateJavaScript(jsToInject) { (value, error) in - guard let result = result else { - return - } - - if error != nil { - let userInfo = (error! as NSError).userInfo - self.onConsoleMessage(message: userInfo["WKJavaScriptExceptionMessage"] as? String ?? "", messageLevel: 3) - } - - if value == nil { - result(nil) - return - } - - result(value) + + if value == nil { + completionHandler(nil) + return } + + completionHandler(value) + } + } + + @available(iOS 14.0, *) + public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: String.Encoding.utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld) + + evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + switch (evalResult) { + case .success(let value): + completionHandler(value) + return + case .failure(let error): + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + break + } + + completionHandler(nil) } } @@ -2292,8 +1195,13 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler) } - public func evaluateJavascript(source: String, contentWorldName: String?, result: @escaping FlutterResult) { - injectDeferredObject(source: source, contentWorldName: contentWorldName, withWrapper: nil, result: result) + public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler) + } + + @available(iOS 14.0, *) + public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler) } @available(iOS 14.0, *) @@ -2304,75 +1212,77 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler) } + @available(iOS 14.0, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld) + + callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + var body: [String: Any?] = [ + "value": nil, + "error": nil + ] + + switch (evalResult) { + case .success(let value): + body["value"] = value + break + case .failure(let error): + let userInfo = (error as NSError).userInfo + body["error"] = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3) + break + } + + completionHandler(body) + } + } + @available(iOS 10.3, *) - public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorldName: String?, result: @escaping FlutterResult) { + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) { if let applePayAPIEnabled = options?.applePayAPIEnabled, applePayAPIEnabled { - result(nil) + completionHandler?(nil) } var jsToInject = functionBody - if #available(iOS 14.0, *) { - var contentWorld = WKContentWorld.page - if let contentWorldName = contentWorldName { - contentWorld = getContentWorld(name: contentWorldName) - if !userScriptsContentWorlds.contains(contentWorldName) { - userScriptsContentWorlds.append(contentWorldName) - addSharedPluginUserScriptsInContentWorld(contentWorldName: contentWorldName) - // Add only the first time all the plugin user scripts needed. - // In the next page load, it will use the WKUserScripts loaded - jsToInject = getAllPluginUserScriptMergedJS() + "\n" + jsToInject - } - } - callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in - var body: [String: Any?] = [ - "value": nil, - "error": nil - ] - - switch (evalResult) { - case .success(let value): - body["value"] = value - break - case .failure(let error): - body["error"] = error - break - } - - result(body) - } - } else { - let resultUuid = NSUUID().uuidString - callAsyncJavaScriptBelowIOS14Results[resultUuid] = result - - var functionArgumentNamesList: [String] = [] - var functionArgumentValuesList: [String] = [] - let keys = arguments.keys - keys.forEach { (key) in - functionArgumentNamesList.append(key) - functionArgumentValuesList.append("obj.\(key)") - } - - let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") - let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") - - jsToInject = callAsyncJavaScriptBelowIOS14WrapperJS - .replacingOccurrences(of: "$FUNCTION_ARGUMENT_NAMES", with: functionArgumentNames) - .replacingOccurrences(of: "$FUNCTION_ARGUMENT_VALUES", with: functionArgumentValues) - .replacingOccurrences(of: "$FUNCTION_ARGUMENTS_OBJ", with: JSONStringify(value: arguments)) - .replacingOccurrences(of: "$FUNCTION_BODY", with: jsToInject) - .replacingOccurrences(of: "$RESULT_UUID", with: resultUuid) - - evaluateJavaScript(jsToInject) { (value, error) in - if error != nil { - let userInfo = (error! as NSError).userInfo - self.onConsoleMessage(message: - userInfo["WKJavaScriptExceptionMessage"] as? String ?? - userInfo["NSLocalizedDescription"] as? String ?? - "", - messageLevel: 3) - result(nil) - self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) - } + + let resultUuid = NSUUID().uuidString + if let completionHandler = completionHandler { + callAsyncJavaScriptBelowIOS14Results[resultUuid] = completionHandler + } + + var functionArgumentNamesList: [String] = [] + var functionArgumentValuesList: [String] = [] + let keys = arguments.keys + keys.forEach { (key) in + functionArgumentNamesList.append(key) + functionArgumentValuesList.append("obj.\(key)") + } + + let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") + let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") + + jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments)) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject) + .replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid) + + evaluateJavaScript(jsToInject) { (value, error) in + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + completionHandler?(nil) + self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) } } } @@ -2409,12 +1319,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);" - injectDeferredObject(source: urlFile, contentWorldName: nil, withWrapper: jsWrapper, result: nil) + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) } public func injectCSSCode(source: String) { let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.body.appendChild(style); })(document);" - injectDeferredObject(source: source, contentWorldName: nil, withWrapper: jsWrapper, result: nil) + injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil) } public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) { @@ -2447,7 +1357,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.body.appendChild(link); })(document);" - injectDeferredObject(source: urlFile, contentWorldName: nil, withWrapper: jsWrapper, result: nil) + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) } public func getCopyBackForwardList() -> [String: Any] { @@ -2492,7 +1402,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi if navigationAction.request.url != nil { - if activateShouldOverrideUrlLoading, let useShouldOverrideUrlLoading = options?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading { + if let useShouldOverrideUrlLoading = options?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading { shouldOverrideUrlLoading(navigationAction: navigationAction, result: { (result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") @@ -2500,7 +1410,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return } else if (result as? NSObject) == FlutterMethodNotImplemented { - self.updateUrlTextFieldForIABController(navigationAction: navigationAction) decisionHandler(.allow) return } @@ -2508,45 +1417,23 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi var response: [String: Any] if let r = result { response = r as! [String: Any] - var action = response["action"] as? Int - action = action != nil ? action : 0; - switch action { - case 1: - self.updateUrlTextFieldForIABController(navigationAction: navigationAction) - decisionHandler(.allow) - break - default: - decisionHandler(.cancel) - } + let action = response["action"] as? Int + let navigationActionPolicy = WKNavigationActionPolicy.init(rawValue: action ?? WKNavigationActionPolicy.cancel.rawValue) ?? + WKNavigationActionPolicy.cancel + decisionHandler(navigationActionPolicy) return; } - self.updateUrlTextFieldForIABController(navigationAction: navigationAction) decisionHandler(.allow) } }) return } - - updateUrlTextFieldForIABController(navigationAction: navigationAction) - } - - if !activateShouldOverrideUrlLoading { - activateShouldOverrideUrlLoading = true } decisionHandler(.allow) } - public func updateUrlTextFieldForIABController(navigationAction: WKNavigationAction) { - if navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .backForward { - currentURL = url - if IABController != nil { - IABController!.updateUrlTextField(url: currentURL?.absoluteString ?? "") - } - } - } - public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { @@ -2608,36 +1495,27 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - self.x509CertificateData = nil + initializeWindowIdJS() - self.startPageTime = currentTimeInMilliSeconds() + if #available(iOS 14.0, *) { + configuration.userContentController.resetContentWorlds(windowId: windowId) + } onLoadStart(url: url?.absoluteString) - if IABController != nil { - // loading url, start spinner, update back/forward - IABController!.backButton.isEnabled = canGoBack - IABController!.forwardButton.isEnabled = canGoForward - - if (IABController!.browserOptions?.spinner)! { - IABController!.spinner.startAnimating() - } - } + inAppBrowserDelegate?.didStartNavigation(url: url) } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + initializeWindowIdJS() + currentURL = url InAppWebView.credentialsProposed = [] - evaluateJavaScript(platformReadyJS, completionHandler: nil) + evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil) onLoadStop(url: url?.absoluteString) - if IABController != nil { - IABController!.updateUrlTextField(url: currentURL?.absoluteString ?? "") - IABController!.backButton.isEnabled = canGoBack - IABController!.forwardButton.isEnabled = canGoForward - IABController!.spinner.stopAnimating() - } + inAppBrowserDelegate?.didFinishNavigation(url: url) } public func webView(_ view: WKWebView, @@ -2661,11 +1539,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onLoadError(url: urlError, error: error) - if IABController != nil { - IABController!.backButton.isEnabled = canGoBack - IABController!.forwardButton.isEnabled = canGoForward - IABController!.spinner.stopAnimating() - } + inAppBrowserDelegate?.didFailNavigation(url: url, error: error) } public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { @@ -2804,19 +1678,24 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi let certificatePath = response["certificatePath"] as! String; let certificatePassword = response["certificatePassword"] as? String ?? ""; - let key = SwiftFlutterPlugin.instance!.registrar!.lookupKey(forAsset: certificatePath) - let path = Bundle.main.path(forResource: key, ofType: nil)! - let PKCS12Data = NSData(contentsOfFile: path)! - - if let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) { - let urlCredential: URLCredential = URLCredential( - identity: identityAndTrust.identityRef, - certificates: identityAndTrust.certArray as? [AnyObject], - persistence: URLCredential.Persistence.forSession); - completionHandler(.useCredential, urlCredential) - } else { + do { + let path = try Util.getAbsPathAsset(assetFilePath: certificatePath) + let PKCS12Data = NSData(contentsOfFile: path)! + + if let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) { + let urlCredential: URLCredential = URLCredential( + identity: identityAndTrust.identityRef, + certificates: identityAndTrust.certArray as? [AnyObject], + persistence: URLCredential.Persistence.forSession); + completionHandler(.useCredential, urlCredential) + } else { + completionHandler(.performDefaultHandling, nil) + } + } catch { + print(error.localizedDescription) completionHandler(.performDefaultHandling, nil) } + break case 2: completionHandler(.cancelAuthenticationChallenge, nil) @@ -2887,11 +1766,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi _ in completionHandler()} ); - if let presentingViewController = ((self.IABController != nil) ? self.IABController! : self.window?.rootViewController!) { - presentingViewController.present(alertController, animated: true, completion: {}) - } else { + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { completionHandler() + return } + presentingViewController.present(alertController, animated: true, completion: {}) } public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, @@ -2943,21 +1822,21 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") - let alertController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) + let confirmController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in + confirmController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in completionHandler(true) })) - alertController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in + confirmController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in completionHandler(false) })) - if let presentingViewController = ((self.IABController != nil) ? self.IABController! : self.window?.rootViewController!) { - presentingViewController.present(alertController, animated: true, completion: nil) - } else { + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { completionHandler(false) + return } + presentingViewController.present(confirmController, animated: true, completion: nil) } public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, @@ -3008,32 +1887,32 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") - let alertController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) + let promptController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) - alertController.addTextField { (textField) in + promptController.addTextField { (textField) in textField.text = defaultValue } - alertController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in + promptController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in if let v = value { completionHandler(v) } - else if let text = alertController.textFields?.first?.text { + else if let text = promptController.textFields?.first?.text { completionHandler(text) } else { completionHandler("") } })) - alertController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in + promptController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in completionHandler(nil) })) - if let presentingViewController = ((self.IABController != nil) ? self.IABController! : self.window?.rootViewController!) { - presentingViewController.present(alertController, animated: true, completion: nil) - } else { + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { completionHandler(nil) + return } + presentingViewController.present(promptController, animated: true, completion: nil) } public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, @@ -3114,7 +1993,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi InAppWebView.windowAutoincrementId += 1 let windowId = InAppWebView.windowAutoincrementId - let windowWebView = InAppWebView(frame: CGRect.zero, configuration: configuration, IABController: nil, contextMenu: nil, channel: nil) + let windowWebView = InAppWebView(frame: CGRect.zero, configuration: configuration, contextMenu: nil, channel: nil) windowWebView.windowId = windowId let webViewTransport = WebViewTransport( @@ -3125,29 +2004,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi InAppWebView.windowWebViews[windowId] = webViewTransport windowWebView.stopLoading() - var iosAllowsConstrainedNetworkAccess: Bool? = nil - var iosAllowsExpensiveNetworkAccess: Bool? = nil - if #available(iOS 13.0, *) { - iosAllowsConstrainedNetworkAccess = navigationAction.request.allowsConstrainedNetworkAccess - iosAllowsExpensiveNetworkAccess = navigationAction.request.allowsExpensiveNetworkAccess - } - - let arguments: [String: Any?] = [ - "url": navigationAction.request.url?.absoluteString, - "windowId": windowId, - "androidIsDialog": nil, - "androidIsUserGesture": nil, - "iosWKNavigationType": navigationAction.navigationType.rawValue, - "iosIsForMainFrame": navigationAction.targetFrame?.isMainFrame ?? false, - "iosAllowsCellularAccess": navigationAction.request.allowsCellularAccess, - "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, - "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, - "iosCachePolicy": navigationAction.request.cachePolicy.rawValue, - "iosHttpShouldHandleCookies": navigationAction.request.httpShouldHandleCookies, - "iosHttpShouldUsePipelining": navigationAction.request.httpShouldUsePipelining, - "iosNetworkServiceType": navigationAction.request.networkServiceType.rawValue, - "iosTimeoutInterval": navigationAction.request.timeoutInterval, - ] + var arguments: [String: Any?] = navigationAction.toMap() + arguments["windowId"] = windowId + arguments["iosWindowFeatures"] = windowFeatures.toMap() + channel?.invokeMethod("onCreateWindow", arguments: arguments, result: { (result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") @@ -3157,7 +2017,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return } else if (result as? NSObject) == FlutterMethodNotImplemented { - self.updateUrlTextFieldForIABController(navigationAction: navigationAction) if InAppWebView.windowWebViews[windowId] != nil { InAppWebView.windowWebViews.removeValue(forKey: windowId) } @@ -3170,9 +2029,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } if !handledByClient, InAppWebView.windowWebViews[windowId] != nil { InAppWebView.windowWebViews.removeValue(forKey: windowId) - if let url = navigationAction.request.url { - self.loadUrl(url: url, headers: nil, allowingReadAccessTo: nil) - } + self.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil) } } }) @@ -3378,140 +2235,40 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi channel?.invokeMethod("onDownloadStart", arguments: arguments) } - public func onLoadResourceCustomScheme(scheme: String, url: String, result: FlutterResult?) { - let arguments: [String: Any] = ["scheme": scheme, "url": url] + public func onLoadResourceCustomScheme(url: String, result: FlutterResult?) { + let arguments: [String: Any] = ["url": url] channel?.invokeMethod("onLoadResourceCustomScheme", arguments: arguments, result: result) } public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, result: FlutterResult?) { - var iosAllowsConstrainedNetworkAccess: Bool? = nil - var iosAllowsExpensiveNetworkAccess: Bool? = nil - if #available(iOS 13.0, *) { - iosAllowsConstrainedNetworkAccess = navigationAction.request.allowsConstrainedNetworkAccess - iosAllowsExpensiveNetworkAccess = navigationAction.request.allowsExpensiveNetworkAccess - } - let arguments: [String: Any?] = [ - "url": navigationAction.request.url?.absoluteString, - "method": navigationAction.request.httpMethod, - "headers": navigationAction.request.allHTTPHeaderFields, - "isForMainFrame": navigationAction.targetFrame?.isMainFrame ?? false, - "androidHasGesture": nil, - "androidIsRedirect": nil, - "iosWKNavigationType": navigationAction.navigationType.rawValue, - "iosAllowsCellularAccess": navigationAction.request.allowsCellularAccess, - "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, - "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, - "iosCachePolicy": navigationAction.request.cachePolicy.rawValue, - "iosHttpShouldHandleCookies": navigationAction.request.httpShouldHandleCookies, - "iosHttpShouldUsePipelining": navigationAction.request.httpShouldUsePipelining, - "iosNetworkServiceType": navigationAction.request.networkServiceType.rawValue, - "iosTimeoutInterval": navigationAction.request.timeoutInterval, - ] - channel?.invokeMethod("shouldOverrideUrlLoading", arguments: arguments, result: result) + channel?.invokeMethod("shouldOverrideUrlLoading", arguments: navigationAction.toMap(), result: result) } public func onNavigationResponse(navigationResponse: WKNavigationResponse, result: FlutterResult?) { - let arguments: [String: Any?] = [ - "url": navigationResponse.response.url?.absoluteString, - "isForMainFrame": navigationResponse.isForMainFrame, - "canShowMIMEType": navigationResponse.canShowMIMEType, - "expectedContentLength": navigationResponse.response.expectedContentLength, - "mimeType": navigationResponse.response.mimeType, - "suggestedFilename": navigationResponse.response.suggestedFilename, - "textEncodingName": navigationResponse.response.textEncodingName, - ] - channel?.invokeMethod("onNavigationResponse", arguments: arguments, result: result) + channel?.invokeMethod("onNavigationResponse", arguments: navigationResponse.toMap(), result: result) } public func onReceivedHttpAuthRequest(challenge: URLAuthenticationChallenge, result: FlutterResult?) { - let arguments: [String: Any?] = [ - "host": challenge.protectionSpace.host, - "protocol": challenge.protectionSpace.protocol, - "realm": challenge.protectionSpace.realm, - "port": challenge.protectionSpace.port, - "previousFailureCount": challenge.previousFailureCount - ] - channel?.invokeMethod("onReceivedHttpAuthRequest", arguments: arguments, result: result) + channel?.invokeMethod("onReceivedHttpAuthRequest", + arguments: HttpAuthenticationChallenge(fromChallenge: challenge).toMap(), result: result) } public func onReceivedServerTrustAuthRequest(challenge: URLAuthenticationChallenge, result: FlutterResult?) { - var serverCertificateData: NSData? - let serverTrust = challenge.protectionSpace.serverTrust! - - var secResult = SecTrustResultType.invalid - let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); - - if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { - let serverCertificateCFData = SecCertificateCopyData(serverCertificate) - let data = CFDataGetBytePtr(serverCertificateCFData) - let size = CFDataGetLength(serverCertificateCFData) - serverCertificateData = NSData(bytes: data, length: size) - if (x509CertificateData == nil) { - x509CertificateData = Data(serverCertificateData!) - InAppWebView.sslCertificateMap[challenge.protectionSpace.host] = x509CertificateData; - } + if let scheme = challenge.protectionSpace.protocol, scheme == "https", + let sslCertificate = challenge.protectionSpace.sslCertificate { + InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate } - - let error = secResult != SecTrustResultType.proceed ? secResult.rawValue : nil - - var message = "" - switch secResult { - case .deny: - message = "Indicates a user-configured deny; do not proceed." - break - case .fatalTrustFailure: - message = "Indicates a trust failure which cannot be overridden by the user." - break - case .invalid: - message = "Indicates an invalid setting or result." - break - case .otherError: - message = "Indicates a failure other than that of trust evaluation." - break - case .recoverableTrustFailure: - message = "Indicates a trust policy failure which can be overridden by the user." - break - case .unspecified: - message = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." - break - default: - message = "" - } - - let arguments: [String: Any?] = [ - "host": challenge.protectionSpace.host, - "protocol": challenge.protectionSpace.protocol, - "realm": challenge.protectionSpace.realm, - "port": challenge.protectionSpace.port, - "previousFailureCount": challenge.previousFailureCount, - "sslCertificate": InAppWebView.getCertificateMap(x509Certificate: - ((serverCertificateData != nil) ? Data(serverCertificateData!) : nil)), - "androidError": nil, - "iosError": error, - "message": message, - ] - channel?.invokeMethod("onReceivedServerTrustAuthRequest", arguments: arguments, result: result) + channel?.invokeMethod("onReceivedServerTrustAuthRequest", + arguments: ServerTrustChallenge(fromChallenge: challenge).toMap(), result: result) } public func onReceivedClientCertRequest(challenge: URLAuthenticationChallenge, result: FlutterResult?) { - let arguments: [String: Any?] = [ - "host": challenge.protectionSpace.host, - "protocol": challenge.protectionSpace.protocol, - "realm": challenge.protectionSpace.realm, - "port": challenge.protectionSpace.port - ] - channel?.invokeMethod("onReceivedClientCertRequest", arguments: arguments, result: result) + channel?.invokeMethod("onReceivedClientCertRequest", + arguments: ClientCertChallenge(fromChallenge: challenge).toMap(), result: result) } public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, result: FlutterResult?) { - let arguments: [String: Any?] = [ - "host": challenge.protectionSpace.host, - "protocol": challenge.protectionSpace.protocol, - "realm": challenge.protectionSpace.realm, - "port": challenge.protectionSpace.port, - "previousFailureCount": challenge.previousFailureCount - ] - channel?.invokeMethod("shouldAllowDeprecatedTLS", arguments: arguments, result: result) + channel?.invokeMethod("shouldAllowDeprecatedTLS", arguments: challenge.toMap(), result: result) } public func onJsAlert(frame: WKFrameInfo, message: String, result: FlutterResult?) { @@ -3562,15 +2319,14 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi channel?.invokeMethod("onTitleChanged", arguments: arguments) } - public func onLongPressHitTestResult(hitTestResult: [String: Any?]) { - let arguments: [String: Any?] = [ - "hitTestResult": hitTestResult - ] - channel?.invokeMethod("onLongPressHitTestResult", arguments: arguments) + public func onLongPressHitTestResult(hitTestResult: HitTestResult) { + channel?.invokeMethod("onLongPressHitTestResult", arguments: hitTestResult.toMap()) } public func onCallJsHandler(handlerName: String, _callHandlerID: Int64, args: String) { let arguments: [String: Any] = ["handlerName": handlerName, "args": args] + + // invoke flutter javascript handler and send back flutter data as a JSON Object to javascript channel?.invokeMethod("onCallJsHandler", arguments: arguments, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") @@ -3731,16 +2487,16 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } public func findAllAsync(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { - let startSearch = "wkwebview_FindAllAsync('\(find ?? "")');" + let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find ?? "")');" evaluateJavaScript(startSearch, completionHandler: completionHandler) } public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { - evaluateJavaScript("wkwebview_FindNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) + evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) } public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { - evaluateJavaScript("wkwebview_ClearMatches();", completionHandler: completionHandler) + evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) } public func scrollTo(x: Int, y: Int, animated: Bool) { @@ -3808,19 +2564,27 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) { if configuration.preferences.javaScriptEnabled { - evaluateJavaScript(getSelectedTextJS, completionHandler: completionHandler) + evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler) } else { completionHandler(nil, nil) } } - public func getHitTestResult(completionHandler: @escaping (Any?, Error?) -> Void) { + public func getHitTestResult(completionHandler: @escaping (HitTestResult) -> Void) { if configuration.preferences.javaScriptEnabled, let lastTouchLocation = lastTouchPoint { self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastTouchLocation.x),\(lastTouchLocation.y))", completionHandler: {(value, error) in - completionHandler(value, error) + if error != nil { + print("getHitTestResult error: \(error?.localizedDescription ?? "")") + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } else if let value = value as? [String: Any?] { + let hitTestResult = HitTestResult.fromMap(map: value)! + completionHandler(hitTestResult) + } else { + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } }) } else { - completionHandler(nil, nil) + completionHandler(HitTestResult(type: .unknownType, extra: nil)) } } @@ -3856,27 +2620,24 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { self.scrollView.subviews.first?.resignFirstResponder() } - public func getCertificate() -> Data? { - var x509Certificate = self.x509CertificateData - if x509Certificate == nil, let scheme = url?.scheme, scheme == "https", - let host = url?.host, let cert = InAppWebView.sslCertificateMap[host] { - x509Certificate = cert + public func getCertificate() -> SslCertificate? { + guard let scheme = url?.scheme, + scheme == "https", + let host = url?.host, + let sslCertificate = InAppWebView.sslCertificatesMap[host] else { + return nil } - return x509Certificate + return sslCertificate } - public func getCertificateMap() -> [String: Any?]? { - return InAppWebView.getCertificateMap(x509Certificate: getCertificate()) - } - - public static func getCertificateMap(x509Certificate: Data?) -> [String: Any?]? { - return x509Certificate != nil ? [ - "issuedBy": nil, - "issuedTo": nil, - "validNotAfterDate": nil, - "validNotBeforeDate": nil, - "x509Certificate": x509Certificate - ] : nil; + public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) { + evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in + if let isSecureContext = isSecureContext { + completionHandler(isSecureContext as? Bool ?? false) + return + } + completionHandler(false) + } } public func dispose() { @@ -3886,27 +2647,16 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } stopLoading() if windowId == nil { - configuration.userContentController.removeScriptMessageHandler(forName: "consoleLog") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleDebug") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleError") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleInfo") - configuration.userContentController.removeScriptMessageHandler(forName: "consoleWarn") - configuration.userContentController.removeScriptMessageHandler(forName: "callHandler") - configuration.userContentController.removeScriptMessageHandler(forName: "onFindResultReceived") + configuration.userContentController.removeAllPluginScriptMessageHandlers() configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") - if #available(iOS 14.0, *) { - configuration.userContentController.removeAllScriptMessageHandlers() - for contentWorldName in userScriptsContentWorlds { - let contentWorld = getContentWorld(name: contentWorldName) - configuration.userContentController.removeAllScriptMessageHandlers(from: contentWorld) - configuration.userContentController.removeAllScriptMessageHandlers(from: contentWorld) - } - } configuration.userContentController.removeAllUserScripts() if #available(iOS 11.0, *) { configuration.userContentController.removeAllContentRuleLists() } + } else if let wId = windowId, InAppWebView.windowWebViews[wId] != nil { + InAppWebView.windowWebViews.removeValue(forKey: wId) } + configuration.userContentController.dispose(windowId: windowId) removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) @@ -3914,19 +2664,18 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { for imp in customIMPs { imp_removeBlock(imp) } - longPressRecognizer?.removeTarget(self, action: #selector(longPressGestureDetected)) - longPressRecognizer?.delegate = nil - scrollView.removeGestureRecognizer(longPressRecognizer!) + longPressRecognizer.removeTarget(self, action: #selector(longPressGestureDetected)) + longPressRecognizer.delegate = nil + scrollView.removeGestureRecognizer(longPressRecognizer) + recognizerForDisablingContextMenuOnLinks.removeTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks.delegate = nil + scrollView.removeGestureRecognizer(recognizerForDisablingContextMenuOnLinks) uiDelegate = nil navigationDelegate = nil scrollView.delegate = nil - IABController?.webView = nil isPausedTimersCompletionHandler = nil channel = nil SharedLastTouchPointTimestamp.removeValue(forKey: self) - if let wId = windowId, InAppWebView.windowWebViews[wId] != nil { - InAppWebView.windowWebViews.removeValue(forKey: wId) - } callAsyncJavaScriptBelowIOS14Results.removeAll() super.removeFromSuperview() } diff --git a/ios/Classes/InAppWebView/InAppWebViewOptions.swift b/ios/Classes/InAppWebView/InAppWebViewOptions.swift index 718f7ca6..28c69094 100755 --- a/ios/Classes/InAppWebView/InAppWebViewOptions.swift +++ b/ios/Classes/InAppWebView/InAppWebViewOptions.swift @@ -68,6 +68,7 @@ public class InAppWebViewOptions: Options { var useOnNavigationResponse = false var applePayAPIEnabled = false var allowingReadAccessTo: String? = nil + var disableLongPressContextMenuOnLinks = false override init(){ super.init() @@ -96,7 +97,7 @@ public class InAppWebViewOptions: Options { if #available(iOS 10.0, *) { realOptions["mediaPlaybackRequiresUserGesture"] = configuration.mediaTypesRequiringUserActionForPlayback == .all realOptions["ignoresViewportScaleLimits"] = configuration.ignoresViewportScaleLimits - realOptions["dataDetectorTypes"] = InAppWebView.getDataDetectorTypeString(type: configuration.dataDetectorTypes) + realOptions["dataDetectorTypes"] = Util.getDataDetectorTypeString(type: configuration.dataDetectorTypes) } else { realOptions["mediaPlaybackRequiresUserGesture"] = configuration.mediaPlaybackRequiresUserAction } @@ -114,7 +115,7 @@ public class InAppWebViewOptions: Options { realOptions["accessibilityIgnoresInvertColors"] = webView.accessibilityIgnoresInvertColors realOptions["contentInsetAdjustmentBehavior"] = webView.scrollView.contentInsetAdjustmentBehavior.rawValue } - realOptions["decelerationRate"] = InAppWebView.getDecelerationRateString(type: webView.scrollView.decelerationRate) + realOptions["decelerationRate"] = Util.getDecelerationRateString(type: webView.scrollView.decelerationRate) realOptions["alwaysBounceVertical"] = webView.scrollView.alwaysBounceVertical realOptions["alwaysBounceHorizontal"] = webView.scrollView.alwaysBounceHorizontal realOptions["scrollsToTop"] = webView.scrollView.scrollsToTop diff --git a/ios/Classes/InAppWebViewMethodHandler.swift b/ios/Classes/InAppWebViewMethodHandler.swift index d8e47f21..67022c30 100644 --- a/ios/Classes/InAppWebViewMethodHandler.swift +++ b/ios/Classes/InAppWebViewMethodHandler.swift @@ -30,27 +30,22 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) break case "loadUrl": - let url = arguments!["url"] as! String - let headers = arguments!["headers"] as! [String: String] - let allowingReadAccessTo = arguments!["iosAllowingReadAccessTo"] as? String + let urlRequest = arguments!["urlRequest"] as! [String:Any?] + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String var allowingReadAccessToURL: URL? = nil if let allowingReadAccessTo = allowingReadAccessTo { allowingReadAccessToURL = URL(string: allowingReadAccessTo) } - webView?.loadUrl(url: URL(string: url)!, headers: headers, allowingReadAccessTo: allowingReadAccessToURL) + webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) result(true) break case "postUrl": - if webView != nil { + if let webView = webView { let url = arguments!["url"] as! String let postData = arguments!["postData"] as! FlutterStandardTypedData - webView!.postUrl(url: URL(string: url)!, postData: postData.data, completionHandler: { () -> Void in - result(true) - }) - } - else { - result(false) + webView.postUrl(url: URL(string: url)!, postData: postData.data) } + result(true) break case "loadData": let data = arguments!["data"] as! String @@ -61,11 +56,10 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(true) break case "loadFile": - let url = arguments!["url"] as! String - let headers = arguments!["headers"] as! [String: String] + let assetFilePath = arguments!["assetFilePath"] as! String do { - try webView?.loadFile(url: url, headers: headers) + try webView?.loadFile(assetFilePath: assetFilePath) } catch let error as NSError { result(FlutterError(code: "InAppWebViewMethodHandler", message: error.domain, details: nil)) @@ -74,10 +68,19 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(true) break case "evaluateJavascript": - if webView != nil { + if let webView = webView { let source = arguments!["source"] as! String - let contentWorldName = arguments!["contentWorld"] as? String - webView!.evaluateJavascript(source: source, contentWorldName: contentWorldName, result: result) + let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] + if #available(iOS 14.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + webView.evaluateJavascript(source: source, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + webView.evaluateJavascript(source: source) { (value) in + result(value) + } + } } else { result(nil) @@ -135,9 +138,9 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(webView?.isLoading ?? false) break case "takeScreenshot": - if webView != nil, #available(iOS 11.0, *) { + if let webView = webView, #available(iOS 11.0, *) { let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] - webView!.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in + webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in result(screenshot) }) } @@ -146,7 +149,7 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "setOptions": - if let iabController = webView?.IABController { + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { let inAppBrowserOptions = InAppBrowserOptions() let inAppBrowserOptionsMap = arguments!["options"] as! [String: Any] let _ = inAppBrowserOptions.parse(options: inAppBrowserOptionsMap) @@ -160,32 +163,35 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(true) break case "getOptions": - if let iabController = webView?.IABController { + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { result(iabController.getOptions()) } else { result(webView?.getOptions()) } break case "close": - if let iabController = webView?.IABController { - iabController.close() - result(true) + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.close { + result(true) + } } else { result(FlutterMethodNotImplemented) } break case "show": - if let iabController = webView?.IABController { - iabController.show() - result(true) + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.show { + result(true) + } } else { result(FlutterMethodNotImplemented) } break case "hide": - if let iabController = webView?.IABController { - iabController.hide() - result(true) + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.hide { + result(true) + } } else { result(FlutterMethodNotImplemented) } @@ -194,9 +200,9 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(webView?.getCopyBackForwardList()) break case "findAllAsync": - if webView != nil { + if let webView = webView { let find = arguments!["find"] as! String - webView!.findAllAsync(find: find, completionHandler: {(value, error) in + webView.findAllAsync(find: find, completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "InAppWebViewMethodHandler", message: error?.localizedDescription, details: nil)) return @@ -208,9 +214,9 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "findNext": - if webView != nil { + if let webView = webView { let forward = arguments!["forward"] as! Bool - webView!.findNext(forward: forward, completionHandler: {(value, error) in + webView.findNext(forward: forward, completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "InAppWebViewMethodHandler", message: error?.localizedDescription, details: nil)) return @@ -222,8 +228,8 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "clearMatches": - if webView != nil { - webView!.clearMatches(completionHandler: {(value, error) in + if let webView = webView { + webView.clearMatches(completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "InAppWebViewMethodHandler", message: error?.localizedDescription, details: nil)) return @@ -261,8 +267,8 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(true) break case "printCurrentPage": - if webView != nil { - webView!.printCurrentPage(printCompletionHandler: {(completed, error) in + if let webView = webView { + webView.printCurrentPage(printCompletionHandler: {(completed, error) in if !completed, let err = error { print(err.localizedDescription) result(false) @@ -291,11 +297,11 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(webView?.getScale()) break case "hasOnlySecureContent": - result(webView?.hasOnlySecureContent) + result(webView?.hasOnlySecureContent ?? false) break case "getSelectedText": - if webView != nil { - webView!.getSelectedText { (value, error) in + if let webView = webView { + webView.getSelectedText { (value, error) in if let err = error { print(err.localizedDescription) result("") @@ -309,14 +315,9 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "getHitTestResult": - if webView != nil { - webView!.getHitTestResult { (value, error) in - if let err = error { - print(err.localizedDescription) - result(nil) - return - } - result(value) + if let webView = webView { + webView.getHitTestResult { (hitTestResult) in + result(hitTestResult.toMap()) } } else { @@ -328,17 +329,17 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(true) break case "setContextMenu": - if webView != nil { + if let webView = webView { let contextMenu = arguments!["contextMenu"] as? [String: Any] - webView!.contextMenu = contextMenu + webView.contextMenu = contextMenu result(true) } else { result(false) } break case "requestFocusNodeHref": - if webView != nil { - webView!.requestFocusNodeHref { (value, error) in + if let webView = webView { + webView.requestFocusNodeHref { (value, error) in if let err = error { print(err.localizedDescription) result(nil) @@ -351,8 +352,8 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "requestImageRef": - if webView != nil { - webView!.requestImageRef { (value, error) in + if let webView = webView { + webView.requestImageRef { (value, error) in if let err = error { print(err.localizedDescription) result(nil) @@ -365,54 +366,72 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "getScrollX": - if webView != nil { - result(Int(webView!.scrollView.contentOffset.x)) + if let webView = webView { + result(Int(webView.scrollView.contentOffset.x)) } else { result(nil) } break case "getScrollY": - if webView != nil { - result(Int(webView!.scrollView.contentOffset.y)) + if let webView = webView { + result(Int(webView.scrollView.contentOffset.y)) } else { result(nil) } break case "getCertificate": - result(webView?.getCertificateMap()) + result(webView?.getCertificate()?.toMap()) break case "addUserScript": - let userScript = arguments!["userScript"] as! [String: Any] - let wkUserScript = WKUserScript(source: userScript["source"] as! String, - injectionTime: WKUserScriptInjectionTime.init(rawValue: userScript["injectionTime"] as! Int) ?? .atDocumentStart, - forMainFrameOnly: userScript["iosForMainFrameOnly"] as! Bool) - webView?.addUserScript(wkUserScript: wkUserScript) + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.addUserOnlyScript(userScript) result(true) break case "removeUserScript": let index = arguments!["index"] as! Int - webView?.removeUserScript(at: index) + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.removeUserOnlyScript(at: index, userOnlyScript: userScript) + result(true) + break + case "removeUserScriptsByGroupName": + let groupName = arguments!["groupName"] as! String + webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) result(true) break case "removeAllUserScripts": - webView?.removeAllUserScripts() + webView?.configuration.userContentController.removeAllUserOnlyScripts() result(true) break case "callAsyncJavaScript": - if webView != nil, #available(iOS 10.3, *) { - let functionBody = arguments!["functionBody"] as! String - let functionArguments = arguments!["arguments"] as! [String:Any] - let contentWorldName = arguments!["contentWorld"] as? String - webView!.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorldName: contentWorldName, result: result) + if let webView = webView, #available(iOS 10.3, *) { + if #available(iOS 14.0, *) { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + var contentWorld = WKContentWorld.page + if let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] { + contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + } + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments) { (value) in + result(value) + } + } } else { result(nil) } break case "createPdf": - if webView != nil, #available(iOS 14.0, *) { + if let webView = webView, #available(iOS 14.0, *) { let configuration = arguments!["iosWKPdfConfiguration"] as? [String: Any?] - webView!.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in + webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in result(pdf) }) } @@ -421,8 +440,8 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "createWebArchiveData": - if webView != nil, #available(iOS 14.0, *) { - webView!.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in + if let webView = webView, #available(iOS 14.0, *) { + webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in result(webArchiveData) }) } @@ -431,10 +450,10 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { } break case "saveWebArchive": - if webView != nil, #available(iOS 14.0, *) { + if let webView = webView, #available(iOS 14.0, *) { let filePath = arguments!["filePath"] as! String let autoname = arguments!["autoname"] as! Bool - webView!.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in + webView.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in result(path) }) } @@ -442,6 +461,16 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(nil) } break + case "isSecureContext": + if let webView = webView { + result(webView.isSecureContext(completionHandler: { (isSecureContext) in + result(isSecureContext) + })) + } + else { + result(nil) + } + break default: result(FlutterMethodNotImplemented) break diff --git a/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift b/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift new file mode 100644 index 00000000..5622691a --- /dev/null +++ b/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift @@ -0,0 +1,21 @@ +// +// CallAsyncJavaScriptBelowIOS14WrapperJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS = """ +(function(obj) { + (async function(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES) { + \(PluginScriptsUtil.VAR_FUNCTION_BODY) + })(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES)).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error + '', 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }); + return null; +})(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ)); +""" diff --git a/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift b/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift new file mode 100644 index 00000000..9288c188 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift @@ -0,0 +1,52 @@ +// +// ConsoleLogJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT" + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT = PluginScript( + groupName: CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: CONSOLE_LOG_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"]) + +// the message needs to be concatenated with '' in order to have the same behavior like on Android +let CONSOLE_LOG_JS_SOURCE = """ +(function(console) { + + var oldLogs = { + 'consoleLog': console.log, + 'consoleDebug': console.debug, + 'consoleError': console.error, + 'consoleInfo': console.info, + 'consoleWarn': console.warn + }; + + for (var k in oldLogs) { + (function(oldLog) { + console[oldLog.replace('console', '').toLowerCase()] = function() { + var message = ''; + for (var i in arguments) { + if (message == '') { + message += arguments[i]; + } + else { + message += ' ' + arguments[i]; + } + } + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers[oldLog].postMessage({'message': message, '_windowId': _windowId}); + oldLogs[oldLog].apply(null, arguments); + } + })(k); + } +})(window.console); +""" diff --git a/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift b/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift new file mode 100644 index 00000000..593ba00e --- /dev/null +++ b/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift @@ -0,0 +1,36 @@ +// +// EnableViewportScaleJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT" + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ENABLE_VIEWPORT_SCALE_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" + +let NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" diff --git a/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift b/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift new file mode 100644 index 00000000..b3c22282 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift @@ -0,0 +1,73 @@ +// +// FindElementsAtPointJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT" + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_ELEMENTS_AT_POINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +/** + https://developer.android.com/reference/android/webkit/WebView.HitTestResult + */ +let FIND_ELEMENTS_AT_POINT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { + var hitTestResultType = { + UNKNOWN_TYPE: 0, + PHONE_TYPE: 2, + GEO_TYPE: 3, + EMAIL_TYPE: 4, + IMAGE_TYPE: 5, + SRC_ANCHOR_TYPE: 7, + SRC_IMAGE_ANCHOR_TYPE: 8, + EDIT_TEXT_TYPE: 9 + }; + var element = document.elementFromPoint(x, y); + var data = { + type: 0, + extra: null + }; + while (element) { + if (element.tagName === 'IMG' && element.src) { + if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { + data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; + } else { + data.type = hitTestResultType.IMAGE_TYPE; + } + data.extra = element.src; + break; + } else if (element.tagName === 'A' && element.href) { + if (element.href.indexOf('mailto:') === 0) { + data.type = hitTestResultType.EMAIL_TYPE; + data.extra = element.href.replace('mailto:', ''); + } else if (element.href.indexOf('tel:') === 0) { + data.type = hitTestResultType.PHONE_TYPE; + data.extra = element.href.replace('tel:', ''); + } else if (element.href.indexOf('geo:') === 0) { + data.type = hitTestResultType.GEO_TYPE; + data.extra = element.href.replace('geo:', ''); + } else { + data.type = hitTestResultType.SRC_ANCHOR_TYPE; + data.extra = element.href; + } + break; + } else if ( + (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || + element.tagName === 'TEXTAREA') { + data.type = hitTestResultType.EDIT_TEXT_TYPE + } + element = element.parentNode; + } + return data; +} +""" diff --git a/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift new file mode 100644 index 00000000..ac93adf0 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -0,0 +1,187 @@ +// +// FindTextHighlightJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT" +let FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._searchResultCount" +let FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._currentHighlight" +let FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._isDoneCounting" + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_TEXT_HIGHLIGHT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onFindResultReceived"]) + +let FIND_TEXT_HIGHLIGHT_JS_SOURCE = """ +\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, keyword) { + if (element) { + if (element.nodeType == 3) { + // Text node + + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx, keyword.length)); + span.appendChild(text); + + span.setAttribute( + "id", + "WKWEBVIEW_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ); + span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; + span.setAttribute("style", "color: #000 !important; background: " + backgroundColor + " !important; padding: 0px !important; margin: 0px !important; border: 0px !important;"); + + text = document.createTextNode(value.substr(idx + keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE)++; + elementTmp = document.createTextNode( + value.substr(idx + keyword.length) + ); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } + } else if (element.nodeType == 1) { + // Element node + if ( + element.style.display != "none" && + element.nodeName.toLowerCase() != "select" + ) { + for (var i = element.childNodes.length - 1; i >= 0; i--) { + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement( + element.childNodes[element.childNodes.length - 1 - i], + keyword + ); + } + } + } + } +} + +// the main entry point to start the search +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync = function(keyword) { + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches(); + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement(document.body, keyword.toLowerCase()); + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = true; + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); +} + +// helper function, recursively removes the highlights in elements and their childs +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement = function(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "\(JAVASCRIPT_BRIDGE_NAME)_Highlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text, element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i = element.childNodes.length - 1; i >= 0; i--) { + if (window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches = function() { + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(document.body); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._findNext = function(forward) { + if (\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) <= 0) return; + + var idx = \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) + (forward ? +1 : -1); + idx = + idx < 0 + ? \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) - 1 + : idx >= \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ? 0 + : idx; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = idx; + + var scrollTo = document.getElementById("\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + idx); + if (scrollTo) { + var highlights = document.getElementsByClassName("\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + for (var i = 0; i < highlights.length; i++) { + var span = highlights[i]; + span.style.backgroundColor = "#FFFF00"; + } + scrollTo.style.backgroundColor = "#FF9732"; + + scrollTo.scrollIntoView({ + behavior: "auto", + block: "center" + }); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } +} +""" diff --git a/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift b/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift new file mode 100644 index 00000000..b51ca464 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift @@ -0,0 +1,232 @@ +// +// InterceptAjaxRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptAjaxRequest" + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_AJAX_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) = true; +(function(ajax) { + var send = ajax.prototype.send; + var open = ajax.prototype.open; + var setRequestHeader = ajax.prototype.setRequestHeader; + ajax.prototype._flutter_inappwebview_url = null; + ajax.prototype._flutter_inappwebview_method = null; + ajax.prototype._flutter_inappwebview_isAsync = null; + ajax.prototype._flutter_inappwebview_user = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_password = null; + ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false; + ajax.prototype._flutter_inappwebview_request_headers = {}; + function convertRequestResponse(request, callback) { + if (request.response != null && request.responseType != null) { + switch (request.responseType) { + case 'arraybuffer': + callback(new Uint8Array(request.response)); + return; + case 'blob': + const reader = new FileReader(); + reader.addEventListener('loadend', function() { + callback(new Uint8Array(reader.result)); + }); + reader.readAsArrayBuffer(blob); + return; + case 'document': + callback(request.response.documentElement.outerHTML); + return; + case 'json': + callback(request.response); + return; + }; + } + callback(null); + }; + ajax.prototype.open = function(method, url, isAsync, user, password) { + isAsync = (isAsync != null) ? isAsync : true; + this._flutter_inappwebview_url = url; + this._flutter_inappwebview_method = method; + this._flutter_inappwebview_isAsync = isAsync; + this._flutter_inappwebview_user = user; + this._flutter_inappwebview_password = password; + this._flutter_inappwebview_request_headers = {}; + open.call(this, method, url, isAsync, user, password); + }; + ajax.prototype.setRequestHeader = function(header, value) { + this._flutter_inappwebview_request_headers[header] = value; + setRequestHeader.call(this, header, value); + }; + function handleEvent(e) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders, responseHeaders, + event: { + type: e.type, + loaded: e.loaded, + lengthComputable: e.lengthComputable, + total: e.total + } + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxProgress', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + }); + }); + } + }; + ajax.prototype.send = function(data) { + var self = this; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) { + this._flutter_inappwebview_already_onreadystatechange_wrapped = true; + var onreadystatechange = this.onreadystatechange; + this.onreadystatechange = function() { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == true) { + var headers = this.getAllResponseHeaders(); + var responseHeaders = {}; + if (headers != null) { + var arr = headers.trim().split(/[\\r\\n]+/); + arr.forEach(function (line) { + var parts = line.split(': '); + var header = parts.shift(); + var value = parts.join(': '); + responseHeaders[header] = value; + }); + } + convertRequestResponse(this, function(response) { + var ajaxRequest = { + method: self._flutter_inappwebview_method, + url: self._flutter_inappwebview_url, + isAsync: self._flutter_inappwebview_isAsync, + user: self._flutter_inappwebview_user, + password: self._flutter_inappwebview_password, + withCredentials: self.withCredentials, + headers: self._flutter_inappwebview_request_headers, + readyState: self.readyState, + status: self.status, + responseURL: self.responseURL, + responseType: self.responseType, + response: response, + responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null, + responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null, + statusText: self.statusText, + responseHeaders: responseHeaders + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + } + if (onreadystatechange != null) { + onreadystatechange(); + } + }); + }); + } else if (onreadystatechange != null) { + onreadystatechange(); + } + }; + } + this.addEventListener('loadstart', handleEvent); + this.addEventListener('load', handleEvent); + this.addEventListener('loadend', handleEvent); + this.addEventListener('progress', handleEvent); + this.addEventListener('error', handleEvent); + this.addEventListener('abort', handleEvent); + this.addEventListener('timeout', handleEvent); + var ajaxRequest = { + data: data, + method: this._flutter_inappwebview_method, + url: this._flutter_inappwebview_url, + isAsync: this._flutter_inappwebview_isAsync, + user: this._flutter_inappwebview_user, + password: this._flutter_inappwebview_password, + withCredentials: this.withCredentials, + headers: this._flutter_inappwebview_request_headers, + responseType: this.responseType + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { + if (result != null) { + switch (result.action) { + case 0: + self.abort(); + return; + }; + data = result.data; + self.withCredentials = result.withCredentials; + if (result.responseType != null) { + self.responseType = result.responseType; + }; + for (var header in result.headers) { + var value = result.headers[header]; + var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header]; + if (flutter_inappwebview_value == null) { + self._flutter_inappwebview_request_headers[header] = value; + } else { + self._flutter_inappwebview_request_headers[header] += ', ' + value; + } + setRequestHeader.call(self, header, value); + }; + if ((self._flutter_inappwebview_method != result.method && result.method != null) || (self._flutter_inappwebview_url != result.url && result.url != null)) { + self.abort(); + self.open(result.method, result.url, result.isAsync, result.user, result.password); + return; + } + } + send.call(self, data); + }); + } else { + send.call(this, data); + } + }; +})(window.XMLHttpRequest); +""" diff --git a/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift b/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift new file mode 100644 index 00000000..3fa45c3b --- /dev/null +++ b/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift @@ -0,0 +1,202 @@ +// +// InterceptFetchRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptFetchRequest" + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_FETCH_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) = true; +(function(fetch) { + if (fetch == null) { + return; + } + function convertHeadersToJson(headers) { + var headersObj = {}; + for (var header of headers.keys()) { + var value = headers.get(header); + headersObj[header] = value; + } + return headersObj; + } + function convertJsonToHeaders(headersJson) { + return new Headers(headersJson); + } + function convertBodyToArray(body) { + return new Response(body).arrayBuffer().then(function(arrayBuffer) { + var arr = Array.from(new Uint8Array(arrayBuffer)); + return arr; + }) + } + function convertArrayIntBodyToUint8Array(arrayIntBody) { + return new Uint8Array(arrayIntBody); + } + function convertCredentialsToJson(credentials) { + var credentialsObj = {}; + if (window.FederatedCredential != null && credentials instanceof FederatedCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.protocol = credentials.protocol; + credentialsObj.provider = credentials.provider; + credentialsObj.iconURL = credentials.iconURL; + } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) { + credentialsObj.type = credentials.type; + credentialsObj.id = credentials.id; + credentialsObj.name = credentials.name; + credentialsObj.password = credentials.password; + credentialsObj.iconURL = credentials.iconURL; + } else { + credentialsObj.type = 'default'; + credentialsObj.value = credentials; + } + } + function convertJsonToCredential(credentialsJson) { + var credentials; + if (window.FederatedCredential != null && credentialsJson.type === 'federated') { + credentials = new FederatedCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + protocol: credentialsJson.protocol, + provider: credentialsJson.provider, + iconURL: credentialsJson.iconURL + }); + } else if (window.PasswordCredential != null && credentialsJson.type === 'password') { + credentials = new PasswordCredential({ + id: credentialsJson.id, + name: credentialsJson.name, + password: credentialsJson.password, + iconURL: credentialsJson.iconURL + }); + } else { + credentials = credentialsJson; + } + return credentials; + } + window.fetch = async function(resource, init) { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == true) { + var fetchRequest = { + url: null, + method: null, + headers: null, + body: null, + mode: null, + credentials: null, + cache: null, + redirect: null, + referrer: null, + referrerPolicy: null, + integrity: null, + keepalive: null + }; + if (resource instanceof Request) { + fetchRequest.url = resource.url; + fetchRequest.method = resource.method; + fetchRequest.headers = resource.headers; + fetchRequest.body = resource.body; + fetchRequest.mode = resource.mode; + fetchRequest.credentials = resource.credentials; + fetchRequest.cache = resource.cache; + fetchRequest.redirect = resource.redirect; + fetchRequest.referrer = resource.referrer; + fetchRequest.referrerPolicy = resource.referrerPolicy; + fetchRequest.integrity = resource.integrity; + fetchRequest.keepalive = resource.keepalive; + } else { + fetchRequest.url = resource; + if (init != null) { + fetchRequest.method = init.method; + fetchRequest.headers = init.headers; + fetchRequest.body = init.body; + fetchRequest.mode = init.mode; + fetchRequest.credentials = init.credentials; + fetchRequest.cache = init.cache; + fetchRequest.redirect = init.redirect; + fetchRequest.referrer = init.referrer; + fetchRequest.referrerPolicy = init.referrerPolicy; + fetchRequest.integrity = init.integrity; + fetchRequest.keepalive = init.keepalive; + } + } + if (fetchRequest.headers instanceof Headers) { + fetchRequest.headers = convertHeadersToJson(fetchRequest.headers); + } + fetchRequest.credentials = convertCredentialsToJson(fetchRequest.credentials); + return convertBodyToArray(fetchRequest.body).then(function(body) { + fetchRequest.body = body; + return window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) { + if (result != null) { + switch (result.action) { + case 0: + var controller = new AbortController(); + if (init != null) { + init.signal = controller.signal; + } else { + init = { + signal: controller.signal + }; + } + controller.abort(); + break; + } + resource = (result.url != null) ? result.url : resource; + if (init == null) { + init = {}; + } + if (result.method != null && result.method.length > 0) { + init.method = result.method; + } + if (result.headers != null && Object.keys(result.headers).length > 0) { + init.headers = convertJsonToHeaders(result.headers); + } + if (result.body != null && result.body.length > 0) { + init.body = convertArrayIntBodyToUint8Array(result.body); + } + if (result.mode != null && result.mode.length > 0) { + init.mode = result.mode; + } + if (result.credentials != null) { + init.credentials = convertJsonToCredential(result.credentials); + } + if (result.cache != null && result.cache.length > 0) { + init.cache = result.cache; + } + if (result.redirect != null && result.redirect.length > 0) { + init.redirect = result.redirect; + } + if (result.referrer != null && result.referrer.length > 0) { + init.referrer = result.referrer; + } + if (result.referrerPolicy != null && result.referrerPolicy.length > 0) { + init.referrerPolicy = result.referrerPolicy; + } + if (result.integrity != null && result.integrity.length > 0) { + init.integrity = result.integrity; + } + if (result.keepalive != null) { + init.keepalive = result.keepalive; + } + return fetch(resource, init); + } + return fetch(resource, init); + }); + }); + } else { + return fetch(resource, init); + } + }; +})(window.fetch); +""" diff --git a/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift new file mode 100644 index 00000000..f26f2e92 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -0,0 +1,33 @@ +// +// javaScriptBridgeJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT" + +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: JAVASCRIPT_BRIDGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["callHandler"]) + +let JAVASCRIPT_BRIDGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + var _callHandlerID = setTimeout(function(){}); + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': arguments[0], '_callHandlerID': _callHandlerID, 'args': JSON.stringify(Array.prototype.slice.call(arguments, 1)), '_windowId': _windowId} ); + return new Promise(function(resolve, reject) { + window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = resolve; + }); +}; +""" + +let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; diff --git a/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift b/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift new file mode 100644 index 00000000..4f3442f9 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift @@ -0,0 +1,62 @@ +// +// LastTouchedAnchorOrImageJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT" + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = null; +window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = null; +(function() { + document.addEventListener('touchstart', function(event) { + var target = event.target; + while (target) { + if (target.tagName === 'IMG') { + var img = target; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = { + src: img.src + }; + var parent = img.parentNode; + while (parent) { + if (parent.tagName === 'A') { + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: parent.textContent, + url: parent.href, + src: img.src + }; + break; + } + parent = parent.parentNode; + } + return; + } else if (target.tagName === 'A') { + var link = target; + var images = link.getElementsByTagName('img'); + var img = (images.length > 0) ? images[0] : null; + var imgSrc = (img != null) ? img.src : null; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = (img != null) ? {src: img.src} : window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: link.textContent, + url: link.href, + src: imgSrc + }; + return; + } + target = target.parentNode; + } + }); +})(); +""" diff --git a/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift b/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift new file mode 100644 index 00000000..828de9ef --- /dev/null +++ b/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift @@ -0,0 +1,39 @@ +// +// resourceObserverJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useOnLoadResource" + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_LOAD_RESOURCE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_LOAD_RESOURCE_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) = true; +(function() { + var observer = new PerformanceObserver(function(list) { + list.getEntries().forEach(function(entry) { + if (\(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == null || \(FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE) == true) { + var resource = { + "url": entry.name, + "initiatorType": entry.initiatorType, + "startTime": entry.startTime, + "duration": entry.duration + }; + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", resource); + } + }); + }); + observer.observe({entryTypes: ['resource']}); +})(); +""" diff --git a/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift b/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift new file mode 100644 index 00000000..345b5ece --- /dev/null +++ b/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowBlurEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_WINDOW_BLUR_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_BLUR_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('blur', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); + }); +})(); +""" diff --git a/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift b/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift new file mode 100644 index 00000000..9ebda3e9 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowFocusEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + source: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_FOCUS_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('focus', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); + }); +})(); +""" diff --git a/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift b/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift new file mode 100644 index 00000000..a1c6637d --- /dev/null +++ b/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift @@ -0,0 +1,31 @@ +// +// OriginalViewPortMetaTagContentJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT" + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; +(function() { + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + if (metaTagNode.name === "viewport") { + window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; + } + } +})(); +""" diff --git a/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift b/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift new file mode 100644 index 00000000..51f9c867 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift @@ -0,0 +1,31 @@ +// +// PluginScripts.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class PluginScriptsUtil { + public static let VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE" + public static let VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES" + public static let VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES" + public static let VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ" + public static let VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY" + public static let VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID" + + public static let GET_SELECTED_TEXT_JS_SOURCE = """ +(function(){ + var txt; + if (window.getSelection) { + txt = window.getSelection().toString(); + } else if (window.document.getSelection) { + txt = window.document.getSelection().toString(); + } else if (window.document.selection) { + txt = window.document.selection.createRange().text; + } + return txt; +})(); +""" +} diff --git a/ios/Classes/PluginScriptsJS/PrintJS.swift b/ios/Classes/PluginScriptsJS/PrintJS.swift new file mode 100644 index 00000000..56a7f304 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/PrintJS.swift @@ -0,0 +1,24 @@ +// +// PrintJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT" + +let PRINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PRINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let PRINT_JS_SOURCE = """ +window.print = function() { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onPrint", window.location.href); +} +""" diff --git a/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift b/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift new file mode 100644 index 00000000..d2ef3e2a --- /dev/null +++ b/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift @@ -0,0 +1,27 @@ +// +// PromisePolyfillJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT" + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PROMISE_POLYFILL_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +// https://github.com/tildeio/rsvp.js +let PROMISE_POLYFILL_JS_SOURCE = """ +if (window.Promise == null) { + !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.RSVP={})}(this,function(t){"use strict";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if("function"!=typeof r)throw new TypeError("Callback must be a function");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s { var entersReaderIfAvailable = false var barCollapsingEnabled = false var dismissButtonStyle = 0 //done - var preferredBarTintColor = "" - var preferredControlTintColor = "" + var preferredBarTintColor: String? + var preferredControlTintColor: String? var presentationStyle = 0 //fullscreen var transitionStyle = 0 //crossDissolve @@ -31,6 +31,10 @@ public class SafariBrowserOptions: Options { realOptions["barCollapsingEnabled"] = safariViewController.configuration.barCollapsingEnabled realOptions["dismissButtonStyle"] = safariViewController.dismissButtonStyle.rawValue } + if #available(iOS 10.0, *) { + realOptions["preferredBarTintColor"] = safariViewController.preferredBarTintColor?.hexString + realOptions["preferredControlTintColor"] = safariViewController.preferredControlTintColor?.hexString + } realOptions["presentationStyle"] = safariViewController.modalPresentationStyle.rawValue realOptions["transitionStyle"] = safariViewController.modalTransitionStyle.rawValue } diff --git a/ios/Classes/SafariViewController/SafariViewController.swift b/ios/Classes/SafariViewController/SafariViewController.swift index b77256bc..395f39a3 100755 --- a/ios/Classes/SafariViewController/SafariViewController.swift +++ b/ios/Classes/SafariViewController/SafariViewController.swift @@ -55,21 +55,25 @@ public class SafariViewController: SFSafariViewController, FlutterPlugin, SFSafa func prepareSafariBrowser() { + guard let safariOptions = safariOptions else { + return + } + if #available(iOS 11.0, *) { - self.dismissButtonStyle = SFSafariViewController.DismissButtonStyle(rawValue: (safariOptions?.dismissButtonStyle)!)! + self.dismissButtonStyle = SFSafariViewController.DismissButtonStyle(rawValue: safariOptions.dismissButtonStyle)! } if #available(iOS 10.0, *) { - if !(safariOptions?.preferredBarTintColor.isEmpty)! { - self.preferredBarTintColor = color(fromHexString: (safariOptions?.preferredBarTintColor)!) + if let preferredBarTintColor = safariOptions.preferredBarTintColor, !preferredBarTintColor.isEmpty { + self.preferredBarTintColor = UIColor(hexString: preferredBarTintColor) } - if !(safariOptions?.preferredControlTintColor.isEmpty)! { - self.preferredControlTintColor = color(fromHexString: (safariOptions?.preferredControlTintColor)!) + if let preferredControlTintColor = safariOptions.preferredControlTintColor, !preferredControlTintColor.isEmpty { + self.preferredControlTintColor = UIColor(hexString: preferredControlTintColor) } } - self.modalPresentationStyle = UIModalPresentationStyle(rawValue: (safariOptions?.presentationStyle)!)! - self.modalTransitionStyle = UIModalTransitionStyle(rawValue: (safariOptions?.transitionStyle)!)! + self.modalPresentationStyle = UIModalPresentationStyle(rawValue: safariOptions.presentationStyle)! + self.modalTransitionStyle = UIModalTransitionStyle(rawValue: safariOptions.transitionStyle)! } func close(result: FlutterResult?) { @@ -134,34 +138,6 @@ public class SafariViewController: SFSafariViewController, FlutterPlugin, SFSafa delegate = nil channel!.setMethodCallHandler(nil) } - - // Helper function to convert hex color string to UIColor - // Assumes input like "#00FF00" (#RRGGBB). - // Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string - func color(fromHexString: String, alpha:CGFloat? = 1.0) -> UIColor { - - // Convert hex string to an integer - let hexint = Int(self.intFromHexString(hexStr: fromHexString)) - let red = CGFloat((hexint & 0xff0000) >> 16) / 255.0 - let green = CGFloat((hexint & 0xff00) >> 8) / 255.0 - let blue = CGFloat((hexint & 0xff) >> 0) / 255.0 - let alpha = alpha! - - // Create color object, specifying alpha as well - let color = UIColor(red: red, green: green, blue: blue, alpha: alpha) - return color - } - - func intFromHexString(hexStr: String) -> UInt32 { - var hexInt: UInt32 = 0 - // Create scanner - let scanner: Scanner = Scanner(string: hexStr) - // Tell scanner to skip the # character - scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") - // Scan hex value - scanner.scanHexInt32(&hexInt) - return hexInt - } } class CustomUIActivity : UIActivity { diff --git a/ios/Classes/Types/ClientCertChallenge.swift b/ios/Classes/Types/ClientCertChallenge.swift new file mode 100644 index 00000000..f725e646 --- /dev/null +++ b/ios/Classes/Types/ClientCertChallenge.swift @@ -0,0 +1,22 @@ +// +// ClientCertChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +class ClientCertChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + ] + } +} diff --git a/ios/Classes/Types/HitTestResult.swift b/ios/Classes/Types/HitTestResult.swift new file mode 100644 index 00000000..53307789 --- /dev/null +++ b/ios/Classes/Types/HitTestResult.swift @@ -0,0 +1,44 @@ +// +// HitTestResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public enum HitTestResultType: Int { + case unknownType = 0 + case phoneType = 2 + case geoType = 3 + case emailType = 4 + case imageType = 5 + case srcAnchorType = 7 + case srcImageAnchorType = 8 + case editTextType = 9 +} + +public class HitTestResult: NSObject { + var type: HitTestResultType + var extra: String? + + public init(type: HitTestResultType, extra: String?) { + self.type = type + self.extra = extra + } + + public static func fromMap(map: [String:Any?]?) -> HitTestResult? { + guard let map = map else { + return nil + } + let type = HitTestResultType.init(rawValue: map["type"] as? Int ?? HitTestResultType.unknownType.rawValue) ?? HitTestResultType.unknownType + return HitTestResult(type: type, extra: map["extra"] as? String) + } + + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "extra": extra, + ] + } +} diff --git a/ios/Classes/Types/PluginScript.swift b/ios/Classes/Types/PluginScript.swift new file mode 100644 index 00000000..97ca2834 --- /dev/null +++ b/ios/Classes/Types/PluginScript.swift @@ -0,0 +1,90 @@ +// +// PluginScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit + +public class PluginScript : UserScript { + var requiredInAllContentWorlds = false + var messageHandlerNames: [String] = [] + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(iOS 14.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(iOS 14.0, *) + public init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(iOS 14.0, *) + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + if #available(iOS 14.0, *) { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + @available(iOS 14.0, *) + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + contentWorld: WKContentWorld? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: contentWorld ?? self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } +} diff --git a/ios/Classes/Types/SecCertificate.swift b/ios/Classes/Types/SecCertificate.swift new file mode 100644 index 00000000..06a17970 --- /dev/null +++ b/ios/Classes/Types/SecCertificate.swift @@ -0,0 +1,18 @@ +// +// SecCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension SecCertificate { + var data: Data { + let serverCertificateCFData = SecCertificateCopyData(self) + let data = CFDataGetBytePtr(serverCertificateCFData) + let size = CFDataGetLength(serverCertificateCFData) + let certificateData = NSData(bytes: data, length: size) + return Data(certificateData) + } +} diff --git a/ios/Classes/Types/ServerTrustChallenge.swift b/ios/Classes/Types/ServerTrustChallenge.swift new file mode 100644 index 00000000..a23adb6b --- /dev/null +++ b/ios/Classes/Types/ServerTrustChallenge.swift @@ -0,0 +1,22 @@ +// +// ServerTrustChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +class ServerTrustChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap() + ] + } +} diff --git a/ios/Classes/Types/SslCertificate.swift b/ios/Classes/Types/SslCertificate.swift new file mode 100644 index 00000000..6b9d9a17 --- /dev/null +++ b/ios/Classes/Types/SslCertificate.swift @@ -0,0 +1,30 @@ +// +// SslCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslCertificate: NSObject { + var x509Certificate: Data + var issuedBy: Any? + var issuedTo: Any? + var validNotAfterDate: Any? + var validNotBeforeDate: Any? + + public init(x509Certificate: Data) { + self.x509Certificate = x509Certificate + } + + public func toMap () -> [String:Any?] { + return [ + "x509Certificate": x509Certificate, + "issuedBy": issuedBy, + "issuedTo": issuedTo, + "validNotAfterDate": validNotAfterDate, + "validNotBeforeDate": validNotBeforeDate + ] + } +} diff --git a/ios/Classes/Types/SslError.swift b/ios/Classes/Types/SslError.swift new file mode 100644 index 00000000..37630b1e --- /dev/null +++ b/ios/Classes/Types/SslError.swift @@ -0,0 +1,50 @@ +// +// SslError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslError: NSObject { + var errorType: SecTrustResultType? + var message: String? + + public init(errorType: SecTrustResultType?) { + self.errorType = errorType + + var sslErrorMessage: String? = nil + switch errorType { + case .deny: + sslErrorMessage = "Indicates a user-configured deny; do not proceed." + break + case .fatalTrustFailure: + sslErrorMessage = "Indicates a trust failure which cannot be overridden by the user." + break + case .invalid: + sslErrorMessage = "Indicates an invalid setting or result." + break + case .otherError: + sslErrorMessage = "Indicates a failure other than that of trust evaluation." + break + case .recoverableTrustFailure: + sslErrorMessage = "Indicates a trust policy failure which can be overridden by the user." + break + case .unspecified: + sslErrorMessage = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." + break + default: + sslErrorMessage = nil + } + + self.message = sslErrorMessage + } + + public func toMap () -> [String:Any?] { + return [ + "iosError": errorType?.rawValue, + "message": message + ] + } +} diff --git a/ios/Classes/Types/UIColor.swift b/ios/Classes/Types/UIColor.swift new file mode 100644 index 00000000..4216b7e2 --- /dev/null +++ b/ios/Classes/Types/UIColor.swift @@ -0,0 +1,59 @@ +// +// UIColor.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension UIColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } + + var hexString: String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + let multiplier = CGFloat(255.999999) + + guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { + return nil + } + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + else { + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + } +} diff --git a/ios/Classes/Types/URLAuthenticationChallenge.swift b/ios/Classes/Types/URLAuthenticationChallenge.swift new file mode 100644 index 00000000..b96a9a2e --- /dev/null +++ b/ios/Classes/Types/URLAuthenticationChallenge.swift @@ -0,0 +1,20 @@ +// +// URLAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLAuthenticationChallenge { + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "iosFailureResponse": failureResponse?.toMap(), + "iosError": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap(), + ] + } +} diff --git a/ios/Classes/Types/URLCredential.swift b/ios/Classes/Types/URLCredential.swift new file mode 100644 index 00000000..91cf8ee9 --- /dev/null +++ b/ios/Classes/Types/URLCredential.swift @@ -0,0 +1,26 @@ +// +// URLCredential.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLCredential { + public func toMap () -> [String:Any?] { + var x509Certificates: [Data] = [] + // certificates could be nil!!! + if certificates != nil { + for certificate in certificates { + x509Certificates.append((certificate as! SecCertificate).data) + } + } + return [ + "password": password, + "username": user, + "iosCertificates": x509Certificates, + "iosPersistence": persistence.rawValue + ] + } +} diff --git a/ios/Classes/Types/URLProtectionSpace.swift b/ios/Classes/Types/URLProtectionSpace.swift new file mode 100644 index 00000000..199f6a23 --- /dev/null +++ b/ios/Classes/Types/URLProtectionSpace.swift @@ -0,0 +1,64 @@ +// +// URLProtectionSpace.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLProtectionSpace { + + var x509Certificate: Data? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); + + if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + return serverCertificate.data + } + return nil + } + + var sslCertificate: SslCertificate? { + var sslCertificate: SslCertificate? = nil + if let x509Certificate = x509Certificate { + sslCertificate = SslCertificate(x509Certificate: x509Certificate) + } + return sslCertificate + } + + var sslError: SslError? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + SecTrustEvaluate(serverTrust, &secResult); + + guard let sslErrorType = secResult != SecTrustResultType.proceed ? secResult : nil else { + return nil + } + + return SslError(errorType: sslErrorType) + } + + public func toMap () -> [String:Any?] { + return [ + "host": host, + "procotol": self.protocol, + "realm": realm, + "port": port, + "sslCertificate": sslCertificate?.toMap(), + "sslError": sslError?.toMap(), + "iosAuthenticationMethod": authenticationMethod, + "iosDistinguishedNames": distinguishedNames, + "iosReceivesCredentialSecurely": receivesCredentialSecurely, + "iosIsProxy": isProxy(), + "iosProxyType": proxyType + ] + } +} diff --git a/ios/Classes/Types/URLRequest.swift b/ios/Classes/Types/URLRequest.swift new file mode 100644 index 00000000..336ea693 --- /dev/null +++ b/ios/Classes/Types/URLRequest.swift @@ -0,0 +1,77 @@ +// +// URLRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLRequest { + public init(fromPluginMap: [String:Any?]) { + let url = fromPluginMap["url"] as! String + self.init(url: URL(string: url)!) + + if let method = fromPluginMap["method"] as? String { + httpMethod = method + } + if let body = fromPluginMap["body"] as? FlutterStandardTypedData { + httpBody = body.data + } + if let headers = fromPluginMap["headers"] as? [String:String] { + for (key, value) in headers { + setValue(value, forHTTPHeaderField: key) + } + } + if let iosAllowsCellularAccess = fromPluginMap["iosAllowsCellularAccess"] as? Bool { + allowsCellularAccess = iosAllowsCellularAccess + } + if #available(iOS 13.0, *), let iosAllowsConstrainedNetworkAccess = fromPluginMap["iosAllowsConstrainedNetworkAccess"] as? Bool { + allowsConstrainedNetworkAccess = iosAllowsConstrainedNetworkAccess + } + if #available(iOS 13.0, *), let iosAllowsExpensiveNetworkAccess = fromPluginMap["iosAllowsExpensiveNetworkAccess"] as? Bool { + allowsExpensiveNetworkAccess = iosAllowsExpensiveNetworkAccess + } + if let iosCachePolicy = fromPluginMap["iosCachePolicy"] as? Int { + cachePolicy = CachePolicy.init(rawValue: UInt(iosCachePolicy)) ?? .useProtocolCachePolicy + } + if let iosHttpShouldHandleCookies = fromPluginMap["iosHttpShouldHandleCookies"] as? Bool { + httpShouldHandleCookies = iosHttpShouldHandleCookies + } + if let iosHttpShouldUsePipelining = fromPluginMap["iosHttpShouldUsePipelining"] as? Bool { + httpShouldUsePipelining = iosHttpShouldUsePipelining + } + if let iosNetworkServiceType = fromPluginMap["iosNetworkServiceType"] as? Int { + networkServiceType = NetworkServiceType.init(rawValue: UInt(iosNetworkServiceType)) ?? .default + } + if let iosTimeoutInterval = fromPluginMap["iosTimeoutInterval"] as? Double { + timeoutInterval = iosTimeoutInterval + } + if let iosMainDocumentURL = fromPluginMap["iosMainDocumentURL"] as? String { + mainDocumentURL = URL(string: iosMainDocumentURL)! + } + } + + public func toMap () -> [String:Any?] { + var iosAllowsConstrainedNetworkAccess: Bool? = nil + var iosAllowsExpensiveNetworkAccess: Bool? = nil + if #available(iOS 13.0, *) { + iosAllowsConstrainedNetworkAccess = allowsConstrainedNetworkAccess + iosAllowsExpensiveNetworkAccess = allowsExpensiveNetworkAccess + } + return [ + "url": url?.absoluteString, + "method": httpMethod, + "headers": allHTTPHeaderFields, + "iosAllowsCellularAccess": allowsCellularAccess, + "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, + "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, + "iosCachePolicy": cachePolicy.rawValue, + "iosHttpShouldHandleCookies": httpShouldHandleCookies, + "iosHttpShouldUsePipelining": httpShouldUsePipelining, + "iosNetworkServiceType": networkServiceType.rawValue, + "iosTimeoutInterval": timeoutInterval, + "iosMainDocumentURL": mainDocumentURL?.absoluteString + ] + } +} diff --git a/ios/Classes/Types/URLResponse.swift b/ios/Classes/Types/URLResponse.swift new file mode 100644 index 00000000..66715846 --- /dev/null +++ b/ios/Classes/Types/URLResponse.swift @@ -0,0 +1,20 @@ +// +// URLResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLResponse { + public func toMap () -> [String:Any?] { + return [ + "expectedContentLength": expectedContentLength, + "mimeType": mimeType, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName, + "url": url?.absoluteString + ] + } +} diff --git a/ios/Classes/Types/UserScript.swift b/ios/Classes/Types/UserScript.swift new file mode 100644 index 00000000..c843e2b8 --- /dev/null +++ b/ios/Classes/Types/UserScript.swift @@ -0,0 +1,61 @@ +// +// InAppWebViewUserScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +public class UserScript : WKUserScript { + var groupName: String? + @available(iOS 14.0, *) + lazy var contentWorld: WKContentWorld = WKContentWorld.page + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.groupName = groupName + } + + @available(iOS 14.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(iOS 14.0, *) + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.groupName = groupName + self.contentWorld = contentWorld + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> UserScript? { + guard let map = map else { + return nil + } + + let contentWorldMap = map["contentWorld"] as? [String:Any?] + if #available(iOS 14.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: windowId)! + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["iosForMainFrameOnly"] as! Bool, + in: contentWorld + ) + } + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["iosForMainFrameOnly"] as! Bool + ) + } +} diff --git a/ios/Classes/Types/WKContentWorld.swift b/ios/Classes/Types/WKContentWorld.swift new file mode 100644 index 00000000..9ebade3d --- /dev/null +++ b/ios/Classes/Types/WKContentWorld.swift @@ -0,0 +1,41 @@ +// +// WKContentWorld.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 14.0, *) +extension WKContentWorld { + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + private static var _windowId = [String: Int64?]() + + var windowId: Int64? { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKContentWorld._windowId[tmpAddress] ?? nil + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKContentWorld._windowId[tmpAddress] = newValue + } + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> WKContentWorld? { + guard let map = map else { + return nil + } + var name = map["name"] as! String + name = windowId != nil && name != "page" ? + WKUserContentController.WINDOW_ID_PREFIX + String(windowId!) + "-" + name : + name + let contentWorld = Util.getContentWorld(name: name) + contentWorld.windowId = name != "page" ? windowId : nil + return contentWorld + } +} diff --git a/ios/Classes/Types/WKFrameInfo.swift b/ios/Classes/Types/WKFrameInfo.swift new file mode 100644 index 00000000..042fe419 --- /dev/null +++ b/ios/Classes/Types/WKFrameInfo.swift @@ -0,0 +1,26 @@ +// +// WKFrameInfo.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKFrameInfo { + + public func toMap () -> [String:Any?] { + var securityOrigin: [String:Any?]? = nil + if #available(iOS 9.0, *) { + securityOrigin = self.securityOrigin.toMap() + } + // fix: self.request throws EXC_BREAKPOINT when coming from WKNavigationAction.sourceFrame + let request: URLRequest? = self.value(forKey: "request") as? URLRequest + return [ + "isMainFrame": isMainFrame, + "request": request?.toMap(), + "securityOrigin": securityOrigin + ] + } +} diff --git a/ios/Classes/Types/WKNavigationAction.swift b/ios/Classes/Types/WKNavigationAction.swift new file mode 100644 index 00000000..564a47e8 --- /dev/null +++ b/ios/Classes/Types/WKNavigationAction.swift @@ -0,0 +1,21 @@ +// +// WKNavigationAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationAction { + public func toMap () -> [String:Any?] { + return [ + "request": request.toMap(), + "isForMainFrame": targetFrame?.isMainFrame ?? false, + "iosWKNavigationType": navigationType.rawValue, + "iosSourceFrame": sourceFrame.toMap(), + "iosTargetFrame": targetFrame?.toMap() + ] + } +} diff --git a/ios/Classes/Types/WKNavigationResponse.swift b/ios/Classes/Types/WKNavigationResponse.swift new file mode 100644 index 00000000..0e685291 --- /dev/null +++ b/ios/Classes/Types/WKNavigationResponse.swift @@ -0,0 +1,19 @@ +// +// WKNavigationResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationResponse { + public func toMap () -> [String:Any?] { + return [ + "response": response.toMap(), + "isForMainFrame": isForMainFrame, + "canShowMIMEType": canShowMIMEType, + ] + } +} diff --git a/ios/Classes/Types/WKSecurityOrigin.swift b/ios/Classes/Types/WKSecurityOrigin.swift new file mode 100644 index 00000000..a7e8c714 --- /dev/null +++ b/ios/Classes/Types/WKSecurityOrigin.swift @@ -0,0 +1,20 @@ +// +// WKSecurityOrigin.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +extension WKSecurityOrigin { + public func toMap () -> [String:Any?] { + return [ + "host": host, + "port": port, + "protocol": self.protocol + ] + } +} diff --git a/ios/Classes/Types/WKUserContentController.swift b/ios/Classes/Types/WKUserContentController.swift new file mode 100644 index 00000000..8cc01fe2 --- /dev/null +++ b/ios/Classes/Types/WKUserContentController.swift @@ -0,0 +1,351 @@ +// +// UserContentController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit +import OrderedSet + +extension WKUserContentController { + static var WINDOW_ID_PREFIX = "WINDOW-ID-" + + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + @available(iOS 14.0, *) + private static var _contentWorlds = [String: Set]() + @available(iOS 14.0, *) + var contentWorlds: Set { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._contentWorlds[tmpAddress]! + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._contentWorlds[tmpAddress] = newValue + } + } + + private static var _userOnlyScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var userOnlyScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._userOnlyScripts[tmpAddress]! + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._userOnlyScripts[tmpAddress] = newValue + } + } + + private static var _pluginScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var pluginScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._pluginScripts[tmpAddress]! + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._pluginScripts[tmpAddress] = newValue + } + } + + public func initialize () { + if #available(iOS 14.0, *) { + contentWorlds = Set([WKContentWorld.page]) + } + pluginScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + userOnlyScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + } + + public func dispose (windowId: Int64?) { + if windowId == nil { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + if #available(iOS 14.0, *) { + contentWorlds.removeAll() + WKUserContentController._contentWorlds.removeValue(forKey: tmpAddress) + } + pluginScripts.removeAll() + WKUserContentController._pluginScripts.removeValue(forKey: tmpAddress) + userOnlyScripts.removeAll() + WKUserContentController._userOnlyScripts.removeValue(forKey: tmpAddress) + } + else if #available(iOS 14.0, *), let windowId = windowId { + let contentWorldsToRemove = contentWorlds.filter({ $0.windowId == windowId }) + for contentWorld in contentWorldsToRemove { + contentWorlds.remove(contentWorld) + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + public func sync(scriptMessageHandler: WKScriptMessageHandler) { + let pluginScriptsList = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in pluginScriptsList { + if !containsPluginScript(with: pluginScript.groupName!) { + addUserScript(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + add(scriptMessageHandler, name: messageHandlerName) + } + } + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in contentWorlds { + let pluginScriptWithContentWorld = pluginScript.copyAndSet(contentWorld: contentWorld) + if !containsPluginScript(with: pluginScriptWithContentWorld.groupName!, in: contentWorld) { + addUserScript(pluginScriptWithContentWorld) + for messageHandlerName in pluginScriptWithContentWorld.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + } + } + } + + let userOnlyScriptsList = userOnlyScripts.compactMap({ $0.value }).joined() + for userOnlyScript in userOnlyScriptsList { + if !userScripts.contains(userOnlyScript) { + addUserScript(userOnlyScript) + } + } + } + + public func addUserOnlyScript(_ userOnlyScript: UserScript) { + if #available(iOS 14.0, *) { + contentWorlds.insert(userOnlyScript.contentWorld) + } + userOnlyScripts[userOnlyScript.injectionTime]!.append(userOnlyScript) + } + + public func addUserOnlyScripts(_ userOnlyScripts: [UserScript]) { + for userOnlyScript in userOnlyScripts { + addUserOnlyScript(userOnlyScript) + } + } + + public func addPluginScript(_ pluginScript: PluginScript) { + if #available(iOS 14.0, *) { + contentWorlds.insert(pluginScript.contentWorld) + } + pluginScripts[pluginScript.injectionTime]!.append(pluginScript) + } + + public func addPluginScripts(_ pluginScripts: [PluginScript]) { + for pluginScript in pluginScripts { + addPluginScript(pluginScript) + } + } + + public func getPluginScriptsRequiredInAllContentWorlds() -> [PluginScript] { + return pluginScripts.compactMap({ $0.value }) + .joined() + .filter({ $0.injectionTime == .atDocumentStart && $0.requiredInAllContentWorlds }) + } + + @available(iOS 14.0, *) + public func generateCodeForScriptEvaluation(scriptMessageHandler: WKScriptMessageHandler, source: String, contentWorld: WKContentWorld) -> String { + let (inserted, _) = contentWorlds.insert(contentWorld) + if inserted { + var generatedCode = "" + let pluginScriptsRequired = getPluginScriptsRequiredInAllContentWorlds() + for pluginScript in pluginScriptsRequired { + generatedCode += pluginScript.source + "\n" + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + if let windowId = contentWorld.windowId { + generatedCode += "\(WINDOW_ID_VARIABLE_JS_SOURCE) = \(String(windowId));\n" + } + return generatedCode + "\n" + source + } + return source + } + + public func removeUserOnlyScript(_ userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.remove(userOnlyScript) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeUserOnlyScript(at index: Int, userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.removeObject(at: index) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeAllUserOnlyScripts() { + let allUserOnlyScripts = Array(userOnlyScripts.compactMap({ $0.value }).joined()) + + userOnlyScripts[.atDocumentStart]!.removeAllObjects() + userOnlyScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allUserOnlyScripts) + } + + public func removePluginScript(_ pluginScript: PluginScript) { + pluginScripts[pluginScript.injectionTime]!.remove(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + if #available(iOS 14.0, *) { + for contentWorld in contentWorlds { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + } + } + } + removeUserScript(scriptToRemove: pluginScript) + } + + public func removeAllPluginScripts() { + let allPluginScripts = Array(pluginScripts.compactMap({ $0.value }).joined()) + + pluginScripts[.atDocumentStart]!.removeAllObjects() + pluginScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allPluginScripts) + } + + public func removeAllPluginScriptMessageHandlers() { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in allPluginScripts { + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + } + } + if #available(iOS 14.0, *) { + removeAllScriptMessageHandlers() + for contentWorld in contentWorlds { + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + @available(iOS 14.0, *) + public func resetContentWorlds(windowId: Int64?) { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + let contentWorldsFiltered = contentWorlds.filter({ $0.windowId == windowId && $0 != WKContentWorld.page }) + for contentWorld in contentWorldsFiltered { + var found = false + for script in allUserOnlyScripts { + if script.contentWorld == contentWorld { + found = true + break + } + } + if !found { + contentWorlds.remove(contentWorld) + } + } + } + + private func removeUserScript(scriptToRemove: WKUserScript, shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if script != scriptToRemove { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + private func removeUserScripts(scriptsToRemove: [WKUserScript], shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if !userScripts.contains(script) { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + public func removeUserOnlyScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [UserScript] = [] + for script in allUserOnlyScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func removePluginScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [PluginScript] = [] + for script in allPluginScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func containsPluginScript(with groupName: String) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName { + return true + } + } + return false + } + + @available(iOS 14.0, *) + public func containsPluginScript(with groupName: String, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(iOS 14.0, *) + public func getContentWorlds(with windowId: Int64?) -> Set { + var contentWorldsFiltered = Set([WKContentWorld.page]) + let contentWorlds = Array(self.contentWorlds) + for contentWorld in contentWorlds { + if contentWorld.windowId == windowId { + contentWorldsFiltered.insert(contentWorld) + } + } + return contentWorldsFiltered + } + + // use a copy of self.userScripts to avoid EXC_BREAKPOINT at runtime if self.userScripts gets removed when another code is looping them + private func useCopyOfUserScripts() -> [WKUserScript] { + return Array(self.userScripts) + } +} diff --git a/ios/Classes/Types/WKWindowFeatures.swift b/ios/Classes/Types/WKWindowFeatures.swift new file mode 100644 index 00000000..469e2be1 --- /dev/null +++ b/ios/Classes/Types/WKWindowFeatures.swift @@ -0,0 +1,24 @@ +// +// WKWindowFeatures.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKWindowFeatures { + public func toMap () -> [String:Any?] { + return [ + "allowsResizing": allowsResizing, + "height": height, + "menuBarVisibility": menuBarVisibility, + "statusBarVisibility": statusBarVisibility, + "toolbarsVisibility": toolbarsVisibility, + "width": width, + "x": x, + "y": y + ] + } +} diff --git a/ios/Classes/Types/WebViewTransport.swift b/ios/Classes/Types/WebViewTransport.swift new file mode 100644 index 00000000..7e21f7e6 --- /dev/null +++ b/ios/Classes/Types/WebViewTransport.swift @@ -0,0 +1,18 @@ +// +// WebViewTransport.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class WebViewTransport: NSObject { + var webView: InAppWebView + var request: URLRequest + + init(webView: InAppWebView, request: URLRequest) { + self.webView = webView + self.request = request + } +} diff --git a/ios/Classes/Util.swift b/ios/Classes/Util.swift new file mode 100644 index 00000000..4899c12f --- /dev/null +++ b/ios/Classes/Util.swift @@ -0,0 +1,152 @@ +// +// Util.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 12/02/21. +// + +import Foundation +import WebKit + +var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] + +public class Util { + public static func getUrlAsset(assetFilePath: String) throws -> URL { + let key = SwiftFlutterPlugin.instance!.registrar!.lookupKey(forAsset: assetFilePath) + guard let assetURL = Bundle.main.url(forResource: key, withExtension: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetURL + } + + public static func getAbsPathAsset(assetFilePath: String) throws -> String { + let key = SwiftFlutterPlugin.instance!.registrar!.lookupKey(forAsset: assetFilePath) + guard let assetAbsPath = Bundle.main.path(forResource: key, ofType: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetAbsPath + } + + public static func convertToDictionary(text: String) -> [String: Any]? { + if let data = text.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + } + + public static func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(value) { + let data = try? JSONSerialization.data(withJSONObject: value, options: options) + if data != nil { + if let string = String(data: data!, encoding: .utf8) { + return string + } + } + } + return "" + } + + @available(iOS 14.0, *) + public static func getContentWorld(name: String) -> WKContentWorld { + switch name { + case "defaultClient": + return WKContentWorld.defaultClient + case "page": + return WKContentWorld.page + default: + return WKContentWorld.world(name: name) + } + } + + @available(iOS 10.0, *) + public static func getDataDetectorType(type: String) -> WKDataDetectorTypes { + switch type { + case "NONE": + return WKDataDetectorTypes.init(rawValue: 0) + case "PHONE_NUMBER": + return .phoneNumber + case "LINK": + return .link + case "ADDRESS": + return .address + case "CALENDAR_EVENT": + return .calendarEvent + case "TRACKING_NUMBER": + return .trackingNumber + case "FLIGHT_NUMBER": + return .flightNumber + case "LOOKUP_SUGGESTION": + return .lookupSuggestion + case "SPOTLIGHT_SUGGESTION": + return .spotlightSuggestion + case "ALL": + return .all + default: + return WKDataDetectorTypes.init(rawValue: 0) + } + } + + @available(iOS 10.0, *) + public static func getDataDetectorTypeString(type: WKDataDetectorTypes) -> [String] { + var dataDetectorTypeString: [String] = [] + if type.contains(.all) { + dataDetectorTypeString.append("ALL") + } else { + if type.contains(.phoneNumber) { + dataDetectorTypeString.append("PHONE_NUMBER") + } + if type.contains(.link) { + dataDetectorTypeString.append("LINK") + } + if type.contains(.address) { + dataDetectorTypeString.append("ADDRESS") + } + if type.contains(.calendarEvent) { + dataDetectorTypeString.append("CALENDAR_EVENT") + } + if type.contains(.trackingNumber) { + dataDetectorTypeString.append("TRACKING_NUMBER") + } + if type.contains(.flightNumber) { + dataDetectorTypeString.append("FLIGHT_NUMBER") + } + if type.contains(.lookupSuggestion) { + dataDetectorTypeString.append("LOOKUP_SUGGESTION") + } + if type.contains(.spotlightSuggestion) { + dataDetectorTypeString.append("SPOTLIGHT_SUGGESTION") + } + } + if dataDetectorTypeString.count == 0 { + dataDetectorTypeString = ["NONE"] + } + return dataDetectorTypeString + } + + public static func getDecelerationRate(type: String) -> UIScrollView.DecelerationRate { + switch type { + case "NORMAL": + return .normal + case "FAST": + return .fast + default: + return .normal + } + } + + public static func getDecelerationRateString(type: UIScrollView.DecelerationRate) -> String { + switch type { + case .normal: + return "NORMAL" + case .fast: + return "FAST" + default: + return "NORMAL" + } + } +} diff --git a/ios/Storyboards/WebView.storyboard b/ios/Storyboards/WebView.storyboard index 512edab9..8b733683 100755 --- a/ios/Storyboards/WebView.storyboard +++ b/ios/Storyboards/WebView.storyboard @@ -1,107 +1,49 @@ - - - - + + - + + - - + + - + + + + + + + + + + + + + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - + - + + + + + + diff --git a/ios/flutter_inappwebview.podspec b/ios/flutter_inappwebview.podspec index fc8f4142..43a50918 100755 --- a/ios/flutter_inappwebview.podspec +++ b/ios/flutter_inappwebview.podspec @@ -21,6 +21,8 @@ A new Flutter plugin. # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } s.swift_version = '5.0' + + s.dependency 'OrderedSet', '~>5.0' s.default_subspec = 'Core' diff --git a/lib/flutter_inappwebview.dart b/lib/flutter_inappwebview.dart index f5df52f7..aeef7034 100755 --- a/lib/flutter_inappwebview.dart +++ b/lib/flutter_inappwebview.dart @@ -21,22 +21,4 @@ library flutter_inappwebview; -export 'src/types.dart'; -export 'src/webview.dart'; -export 'src/in_app_webview_controller.dart'; -export 'src/headless_in_app_webview.dart'; -export 'src/in_app_webview.dart'; -export 'src/in_app_browser.dart'; -export 'src/cookie_manager.dart'; -export 'src/chrome_safari_browser.dart'; -export 'src/chrome_safari_browser.dart'; -export 'src/in_app_localhost_server.dart'; -export 'src/webview_options.dart'; -export 'src/content_blocker.dart'; -export 'src/http_auth_credentials_database.dart'; -export 'src/web_storage_manager.dart'; -export 'src/context_menu.dart'; -export 'src/web_storage.dart'; -export 'src/X509Certificate/main.dart'; -export 'src/android/service_worker_controller.dart'; -export 'src/android/webview_feature.dart'; \ No newline at end of file +export 'src/main.dart'; \ No newline at end of file diff --git a/lib/src/_uuid_generator.dart b/lib/src/_uuid_generator.dart new file mode 100644 index 00000000..9d603455 --- /dev/null +++ b/lib/src/_uuid_generator.dart @@ -0,0 +1,3 @@ +import 'package:uuid/uuid.dart'; + +final UUID_GENERATOR = Uuid(); \ No newline at end of file diff --git a/lib/src/android/main.dart b/lib/src/android/main.dart new file mode 100644 index 00000000..2ae39c51 --- /dev/null +++ b/lib/src/android/main.dart @@ -0,0 +1,2 @@ +export 'service_worker_controller.dart'; +export 'webview_feature.dart'; diff --git a/lib/src/android/service_worker_controller.dart b/lib/src/android/service_worker_controller.dart index cdb85a16..d4bfb7c0 100644 --- a/lib/src/android/service_worker_controller.dart +++ b/lib/src/android/service_worker_controller.dart @@ -32,25 +32,12 @@ class AndroidServiceWorkerController { switch (call.method) { case "shouldInterceptRequest": - String url = call.arguments["url"]; - String method = call.arguments["method"]; - Map? headers = - call.arguments["headers"]?.cast(); - bool isForMainFrame = call.arguments["isForMainFrame"]; - bool hasGesture = call.arguments["hasGesture"]; - bool isRedirect = call.arguments["isRedirect"]; - - var request = new WebResourceRequest( - url: url, - method: method, - headers: headers, - isForMainFrame: isForMainFrame, - hasGesture: hasGesture, - isRedirect: isRedirect); - if (serviceWorkerClient != null && serviceWorkerClient.shouldInterceptRequest != null) { + Map arguments = call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + return (await serviceWorkerClient.shouldInterceptRequest!(request)) - ?.toMap(); + ?.toMap(); } break; default: diff --git a/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart b/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart new file mode 100755 index 00000000..3350acf2 --- /dev/null +++ b/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart @@ -0,0 +1,87 @@ +import 'dart:ui'; + +import '../../util.dart'; + +import '../chrome_safari_browser_options.dart'; +import '../chrome_safari_browser.dart'; + +import '../../in_app_webview/android/in_app_webview_options.dart'; + +///This class represents all the Android-only [ChromeSafariBrowser] options available. +class AndroidChromeCustomTabsOptions + implements ChromeSafariBrowserOptions, AndroidOptions { + ///Set to `false` if you don't want the default share item to the menu. The default value is `true`. + bool addDefaultShareMenuItem; + + ///Set to `false` if the title shouldn't be shown in the custom tab. The default value is `true`. + bool showTitle; + + ///Set the custom background color of the toolbar. + Color? toolbarBackgroundColor; + + ///Set to `true` to enable the url bar to hide as the user scrolls down on the page. The default value is `false`. + bool enableUrlBarHiding; + + ///Set to `true` to enable Instant Apps. The default value is `false`. + bool instantAppsEnabled; + + ///Set an explicit application package name that limits + ///the components this Intent will resolve to. If left to the default + ///value of null, all components in all applications will considered. + ///If non-null, the Intent can only match the components in the given + ///application package. + String? packageName; + + ///Set to `true` to enable Keep Alive. The default value is `false`. + bool keepAliveEnabled; + + AndroidChromeCustomTabsOptions( + {this.addDefaultShareMenuItem = true, + this.showTitle = true, + this.toolbarBackgroundColor, + this.enableUrlBarHiding = false, + this.instantAppsEnabled = false, + this.packageName, + this.keepAliveEnabled = false}); + + @override + Map toMap() { + return { + "addDefaultShareMenuItem": addDefaultShareMenuItem, + "showTitle": showTitle, + "toolbarBackgroundColor": toolbarBackgroundColor?.toHex(), + "enableUrlBarHiding": enableUrlBarHiding, + "instantAppsEnabled": instantAppsEnabled, + "packageName": packageName, + "keepAliveEnabled": keepAliveEnabled + }; + } + + static AndroidChromeCustomTabsOptions fromMap(Map map) { + AndroidChromeCustomTabsOptions options = + new AndroidChromeCustomTabsOptions(); + options.addDefaultShareMenuItem = map["addDefaultShareMenuItem"]; + options.showTitle = map["showTitle"]; + options.toolbarBackgroundColor = UtilColor.fromHex(map["toolbarBackgroundColor"]); + options.enableUrlBarHiding = map["enableUrlBarHiding"]; + options.instantAppsEnabled = map["instantAppsEnabled"]; + options.packageName = map["packageName"]; + options.keepAliveEnabled = map["keepAliveEnabled"]; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + AndroidChromeCustomTabsOptions copy() { + return AndroidChromeCustomTabsOptions.fromMap(this.toMap()); + } +} \ No newline at end of file diff --git a/lib/src/chrome_safari_browser/android/main.dart b/lib/src/chrome_safari_browser/android/main.dart new file mode 100644 index 00000000..484176f5 --- /dev/null +++ b/lib/src/chrome_safari_browser/android/main.dart @@ -0,0 +1 @@ +export 'chrome_custom_tabs_options.dart'; diff --git a/lib/src/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart similarity index 71% rename from lib/src/chrome_safari_browser.dart rename to lib/src/chrome_safari_browser/chrome_safari_browser.dart index c9c30308..d33a1d21 100755 --- a/lib/src/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -3,28 +3,25 @@ import 'dart:collection'; import 'package:flutter/services.dart'; -import 'types.dart'; -import 'in_app_browser.dart'; +import '../_uuid_generator.dart'; +import 'chrome_safari_browser_options.dart'; ///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. -/// -///**NOTE**: If you want to use the `ChromeSafariBrowser` class on Android 11+ you need to specify your app querying for `android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml` (you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above). +///**NOTE**: If you want to use the `ChromeSafariBrowser` class on Android 11+ you need to specify your app querying for +///`android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml` +///(you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above). class ChromeSafariBrowser { late String uuid; - InAppBrowser? browserFallback; Map _menuItems = new HashMap(); bool _isOpened = false; late MethodChannel _channel; static const MethodChannel _sharedChannel = const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser'); - ///Initialize the [ChromeSafariBrowser] instance with an [InAppBrowser] fallback instance or `null`. - ChromeSafariBrowser({bFallback}) { - uuid = uuidGenerator.v4(); - browserFallback = bFallback; + ChromeSafariBrowser() { + uuid = UUID_GENERATOR.v4(); this._channel = MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$uuid'); this._channel.setMethodCallHandler(handleMethod); @@ -58,21 +55,13 @@ class ChromeSafariBrowser { ///Opens an [url] in a new [ChromeSafariBrowser] instance. /// - ///[url]: The [url] to load. Call [encodeUriComponent()] on this if the [url] contains Unicode characters. + ///[url]: The [url] to load. /// ///[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. - /// - ///[contextMenuFallback]: Context Menu used by the [InAppBrowser] instance fallback. Future open( - {required String url, - ChromeSafariBrowserClassOptions? options, - Map? headersFallback = const {}, - InAppBrowserClassOptions? optionsFallback}) async { - assert(url.isNotEmpty); + {required Uri url, + ChromeSafariBrowserClassOptions? options}) async { + assert(url.toString().isNotEmpty); this.throwIsAlreadyOpened(message: 'Cannot open $url!'); List> menuItemList = []; @@ -82,15 +71,9 @@ class ChromeSafariBrowser { Map args = {}; args.putIfAbsent('uuid', () => uuid); - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); args.putIfAbsent('options', () => options?.toMap() ?? {}); args.putIfAbsent('menuItemList', () => menuItemList); - args.putIfAbsent('uuidFallback', () => browserFallback?.uuid); - args.putIfAbsent('headersFallback', () => headersFallback ?? {}); - args.putIfAbsent('optionsFallback', () => optionsFallback?.toMap() ?? {}); - args.putIfAbsent('contextMenuFallback', - () => browserFallback?.contextMenu?.toMap() ?? {}); - args.putIfAbsent('initialUserScriptsFallback', () => browserFallback?.initialUserScripts?.map((e) => e.toMap()).toList() ?? []); await _sharedChannel.invokeMethod('open', args); this._isOpened = true; } diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser_options.dart b/lib/src/chrome_safari_browser/chrome_safari_browser_options.dart new file mode 100755 index 00000000..8b8b91ca --- /dev/null +++ b/lib/src/chrome_safari_browser/chrome_safari_browser_options.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; + +import 'android/chrome_custom_tabs_options.dart'; +import 'ios/safari_options.dart'; + +class ChromeSafariBrowserOptions { + Map toMap() { + return {}; + } + + static ChromeSafariBrowserOptions fromMap(Map map) { + return new ChromeSafariBrowserOptions(); + } + + ChromeSafariBrowserOptions copy() { + return ChromeSafariBrowserOptions.fromMap(this.toMap()); + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Class that represents the options that can be used for an [ChromeSafariBrowser] window. +class ChromeSafariBrowserClassOptions { + ///Android-specific options. + AndroidChromeCustomTabsOptions? android; + + ///iOS-specific options. + IOSSafariOptions? ios; + + ChromeSafariBrowserClassOptions({this.android, this.ios}) { + this.android = this.android ?? AndroidChromeCustomTabsOptions(); + this.ios = this.ios ?? IOSSafariOptions(); + } + + Map toMap() { + Map options = {}; + if (defaultTargetPlatform == TargetPlatform.android) + options.addAll(this.android?.toMap() ?? {}); + else if (defaultTargetPlatform == TargetPlatform.iOS) options.addAll(this.ios?.toMap() ?? {}); + + return options; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} \ No newline at end of file diff --git a/lib/src/chrome_safari_browser/ios/main.dart b/lib/src/chrome_safari_browser/ios/main.dart new file mode 100644 index 00000000..35573726 --- /dev/null +++ b/lib/src/chrome_safari_browser/ios/main.dart @@ -0,0 +1 @@ +export 'safari_options.dart'; \ No newline at end of file diff --git a/lib/src/chrome_safari_browser/ios/safari_options.dart b/lib/src/chrome_safari_browser/ios/safari_options.dart new file mode 100755 index 00000000..1eee87a9 --- /dev/null +++ b/lib/src/chrome_safari_browser/ios/safari_options.dart @@ -0,0 +1,91 @@ +import 'dart:ui'; + +import '../../util.dart'; +import '../../types.dart'; + +import '../chrome_safari_browser_options.dart'; +import '../chrome_safari_browser.dart'; + +import '../../in_app_webview/ios/in_app_webview_options.dart'; + +///This class represents all the iOS-only [ChromeSafariBrowser] options available. +class IOSSafariOptions implements ChromeSafariBrowserOptions, IosOptions { + ///Set to `true` if Reader mode should be entered automatically when it is available for the webpage. The default value is `false`. + bool entersReaderIfAvailable; + + ///Set to `true` to enable bar collapsing. The default value is `false`. + bool barCollapsingEnabled; + + ///Set the custom style for the dismiss button. The default value is [IOSSafariDismissButtonStyle.DONE]. + /// + ///**NOTE**: available on iOS 11.0+. + IOSSafariDismissButtonStyle dismissButtonStyle; + + ///Set the custom background color of the navigation bar and the toolbar. + /// + ///**NOTE**: available on iOS 10.0+. + Color? preferredBarTintColor; + + ///Set the custom color of the control buttons on the navigation bar and the toolbar. + /// + ///**NOTE**: available on iOS 10.0+. + Color? preferredControlTintColor; + + ///Set the custom modal presentation style when presenting the WebView. The default value is [IOSUIModalPresentationStyle.FULL_SCREEN]. + IOSUIModalPresentationStyle presentationStyle; + + ///Set to the custom transition style when presenting the WebView. The default value is [IOSUIModalTransitionStyle.COVER_VERTICAL]. + IOSUIModalTransitionStyle transitionStyle; + + IOSSafariOptions( + {this.entersReaderIfAvailable = false, + this.barCollapsingEnabled = false, + this.dismissButtonStyle = IOSSafariDismissButtonStyle.DONE, + this.preferredBarTintColor, + this.preferredControlTintColor, + this.presentationStyle = IOSUIModalPresentationStyle.FULL_SCREEN, + this.transitionStyle = IOSUIModalTransitionStyle.COVER_VERTICAL}); + + @override + Map toMap() { + return { + "entersReaderIfAvailable": entersReaderIfAvailable, + "barCollapsingEnabled": barCollapsingEnabled, + "dismissButtonStyle": dismissButtonStyle.toValue(), + "preferredBarTintColor": preferredBarTintColor?.toHex(), + "preferredControlTintColor": preferredControlTintColor?.toHex(), + "presentationStyle": presentationStyle.toValue(), + "transitionStyle": transitionStyle.toValue() + }; + } + + static IOSSafariOptions fromMap(Map map) { + IOSSafariOptions options = IOSSafariOptions(); + options.entersReaderIfAvailable = map["entersReaderIfAvailable"]; + options.barCollapsingEnabled = map["barCollapsingEnabled"]; + options.dismissButtonStyle = + IOSSafariDismissButtonStyle.fromValue(map["dismissButtonStyle"])!; + options.preferredBarTintColor = UtilColor.fromHex(map["preferredBarTintColor"]); + options.preferredControlTintColor = UtilColor.fromHex(map["preferredControlTintColor"]); + options.presentationStyle = + IOSUIModalPresentationStyle.fromValue(map["presentationStyle"])!; + options.transitionStyle = + IOSUIModalTransitionStyle.fromValue(map["transitionStyle"])!; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + IOSSafariOptions copy() { + return IOSSafariOptions.fromMap(this.toMap()); + } +} diff --git a/lib/src/chrome_safari_browser/main.dart b/lib/src/chrome_safari_browser/main.dart new file mode 100644 index 00000000..dfe4093e --- /dev/null +++ b/lib/src/chrome_safari_browser/main.dart @@ -0,0 +1,4 @@ +export 'chrome_safari_browser.dart'; +export 'chrome_safari_browser_options.dart'; +export 'android/main.dart'; +export 'ios/main.dart'; \ No newline at end of file diff --git a/lib/src/context_menu.dart b/lib/src/context_menu.dart index 5f4ba03b..aaadbfab 100644 --- a/lib/src/context_menu.dart +++ b/lib/src/context_menu.dart @@ -1,6 +1,4 @@ -import 'package:flutter/foundation.dart'; - -import 'webview.dart'; +import 'in_app_webview/webview.dart'; import 'types.dart'; ///Class that represents the WebView context menu. It used by [WebView.contextMenu]. diff --git a/lib/src/cookie_manager.dart b/lib/src/cookie_manager.dart index f45375bd..98191480 100755 --- a/lib/src/cookie_manager.dart +++ b/lib/src/cookie_manager.dart @@ -5,8 +5,10 @@ import 'package:device_info/device_info.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; -import 'in_app_webview_controller.dart'; -import 'headless_in_app_webview.dart'; +import 'in_app_webview/in_app_webview_controller.dart'; +import 'in_app_webview/in_app_webview_options.dart'; +import 'in_app_webview/headless_in_app_webview.dart'; + import 'types.dart'; ///Class that implements a singleton object (shared instance) which manages the cookies used by WebView instances. @@ -50,7 +52,7 @@ class CookieManager { ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to set the cookie (session-only cookie won't work! In that case, you should set also [expiresDate] or [maxAge]). Future setCookie( - {required String url, + {required Uri url, required String name, required String value, String? domain, @@ -63,7 +65,7 @@ class CookieManager { InAppWebViewController? iosBelow11WebViewController}) async { if (domain == null) domain = _getDomainName(url); - assert(url.isNotEmpty); + assert(url.toString().isNotEmpty); assert(name.isNotEmpty); assert(value.isNotEmpty); assert(domain.isNotEmpty); @@ -82,7 +84,7 @@ class CookieManager { } Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); args.putIfAbsent('name', () => name); args.putIfAbsent('value', () => value); args.putIfAbsent('domain', () => domain); @@ -97,7 +99,7 @@ class CookieManager { } Future _setCookieWithJavaScript( - {required String url, + {required Uri url, required String name, required String value, required String domain, @@ -125,8 +127,8 @@ class CookieManager { if (webViewController != null) { InAppWebViewGroupOptions? options = await webViewController.getOptions(); - if (options != null && options.crossPlatform != null && - options.crossPlatform!.javaScriptEnabled) { + if (options != null && + options.crossPlatform.javaScriptEnabled) { await webViewController.evaluateJavascript( source: 'document.cookie="$cookieValue"'); return; @@ -135,7 +137,7 @@ class CookieManager { var setCookieCompleter = Completer(); var headlessWebView = new HeadlessInAppWebView( - initialUrl: url, + initialUrlRequest: URLRequest(url: url), onLoadStop: (controller, url) async { await controller.evaluateJavascript( source: 'document.cookie="$cookieValue"'); @@ -156,8 +158,8 @@ class CookieManager { ///**NOTE for iOS below 11.0**: All the cookies returned this way will have all the properties to `null` except for [Cookie.name] and [Cookie.value]. ///If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to get the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be found!). - Future> getCookies({required String url, InAppWebViewController? iosBelow11WebViewController}) async { - assert(url.isNotEmpty); + Future> getCookies({required Uri url, InAppWebViewController? iosBelow11WebViewController}) async { + assert(url.toString().isNotEmpty); if (Platform.isIOS) { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); @@ -171,7 +173,7 @@ class CookieManager { List cookies = []; Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); List cookieListMap = await _channel.invokeMethod('getCookies', args); cookieListMap = cookieListMap.cast>(); @@ -192,15 +194,15 @@ class CookieManager { return cookies; } - Future> _getCookiesWithJavaScript({required String url, InAppWebViewController? webViewController}) async { - assert(url.isNotEmpty); + Future> _getCookiesWithJavaScript({required Uri url, InAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); List cookies = []; if (webViewController != null) { InAppWebViewGroupOptions? options = await webViewController.getOptions(); - if (options != null && options.crossPlatform != null && - options.crossPlatform!.javaScriptEnabled) { + if (options != null && + options.crossPlatform.javaScriptEnabled) { List documentCookies = (await webViewController.evaluateJavascript(source: 'document.cookie') as String) .split(';').map((documentCookie) => documentCookie.trim()).toList(); documentCookies.forEach((documentCookie) { @@ -217,7 +219,7 @@ class CookieManager { var pageLoaded = Completer(); var headlessWebView = new HeadlessInAppWebView( - initialUrl: url, + initialUrlRequest: URLRequest(url: url), onLoadStop: (controller, url) async { pageLoaded.complete(); }, @@ -249,9 +251,9 @@ class CookieManager { ///If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to get the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be found!). Future getCookie( - {required String url, required String name, InAppWebViewController? iosBelow11WebViewController}) async { + {required Uri url, required String name, InAppWebViewController? iosBelow11WebViewController}) async { - assert(url.isNotEmpty); + assert(url.toString().isNotEmpty); assert(name.isNotEmpty); if (Platform.isIOS) { @@ -265,7 +267,7 @@ class CookieManager { } Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); List cookies = await _channel.invokeMethod('getCookies', args); cookies = cookies.cast>(); for (var i = 0; i < cookies.length; i++) { @@ -298,14 +300,14 @@ class CookieManager { ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to delete the cookie (session-only cookie and cookie with `isHttpOnly` enabled won't be deleted!). Future deleteCookie( - {required String url, + {required Uri url, required String name, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); - assert(url.isNotEmpty); + assert(url.toString().isNotEmpty); assert(name.isNotEmpty); if (Platform.isIOS) { @@ -319,7 +321,7 @@ class CookieManager { } Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); args.putIfAbsent('name', () => name); args.putIfAbsent('domain', () => domain); args.putIfAbsent('path', () => path); @@ -338,11 +340,11 @@ class CookieManager { ///**NOTE for iOS below 11.0**: If [iosBelow11WebViewController] is `null` or JavaScript is disabled for it, it will try to use a [HeadlessInAppWebView] ///to delete the cookies (session-only cookies and cookies with `isHttpOnly` enabled won't be deleted!). Future deleteCookies( - {required String url, String domain = "", String path = "/", + {required Uri url, String domain = "", String path = "/", InAppWebViewController? iosBelow11WebViewController}) async { if (domain.isEmpty) domain = _getDomainName(url); - assert(url.isNotEmpty); + assert(url.toString().isNotEmpty); if (Platform.isIOS) { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); @@ -358,7 +360,7 @@ class CookieManager { } Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); args.putIfAbsent('domain', () => domain); args.putIfAbsent('path', () => path); await _channel.invokeMethod('deleteCookies', args); @@ -372,9 +374,8 @@ class CookieManager { await _channel.invokeMethod('deleteAllCookies', args); } - String _getDomainName(String url) { - Uri uri = Uri.parse(url); - String domain = uri.host; + String _getDomainName(Uri url) { + String domain = url.host; // ignore: unnecessary_null_comparison if (domain == null) return ""; return domain.startsWith("www.") ? domain.substring(4) : domain; diff --git a/lib/src/http_auth_credentials_database.dart b/lib/src/http_auth_credentials_database.dart index b32e0a48..7ae70e9b 100755 --- a/lib/src/http_auth_credentials_database.dart +++ b/lib/src/http_auth_credentials_database.dart @@ -27,37 +27,28 @@ class HttpAuthCredentialDatabase { static Future _handleMethod(MethodCall call) async {} ///Gets a map list of all HTTP auth credentials saved. - ///Each map contains the key `protectionSpace` of type [ProtectionSpace] - ///and the key `credentials` of type `List` that contains all the HTTP auth credentials saved for that `protectionSpace`. - Future> + ///Each map contains the key `protectionSpace` of type [URLProtectionSpace] + ///and the key `credentials` of type `List` that contains all the HTTP auth credentials saved for that `protectionSpace`. + Future> getAllAuthCredentials() async { Map args = {}; List allCredentials = await _channel.invokeMethod('getAllAuthCredentials', args); - List result = []; + List result = []; for (Map map in allCredentials) { - Map protectionSpace = map["protectionSpace"]; - List credentials = map["credentials"]; - result.add(ProtectionSpaceHttpAuthCredentials( - protectionSpace: ProtectionSpace( - host: protectionSpace["host"], - protocol: protectionSpace["protocol"], - realm: protectionSpace["realm"], - port: protectionSpace["port"]), - credentials: credentials - .map((credential) => HttpAuthCredential( - username: credential["username"], - password: credential["password"])) - .toList())); + var element = URLProtectionSpaceHttpAuthCredentials.fromMap(map.cast()); + if (element != null) { + result.add(element); + } } return result; } ///Gets all the HTTP auth credentials saved for that [protectionSpace]. - Future> getHttpAuthCredentials( - {required ProtectionSpace protectionSpace}) async { + Future> getHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { Map args = {}; args.putIfAbsent("host", () => protectionSpace.host); args.putIfAbsent("protocol", () => protectionSpace.protocol); @@ -65,18 +56,20 @@ class HttpAuthCredentialDatabase { args.putIfAbsent("port", () => protectionSpace.port); List credentialList = await _channel.invokeMethod('getHttpAuthCredentials', args); - List credentials = []; - for (Map credential in credentialList) { - credentials.add(HttpAuthCredential( - username: credential["username"], password: credential["password"])); + List credentials = []; + for (Map map in credentialList) { + var credential = URLCredential.fromMap(map.cast()); + if (credential != null) { + credentials.add(credential); + } } return credentials; } ///Saves an HTTP auth [credential] for that [protectionSpace]. Future setHttpAuthCredential( - {required ProtectionSpace protectionSpace, - required HttpAuthCredential credential}) async { + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { Map args = {}; args.putIfAbsent("host", () => protectionSpace.host); args.putIfAbsent("protocol", () => protectionSpace.protocol); @@ -89,8 +82,8 @@ class HttpAuthCredentialDatabase { ///Removes an HTTP auth [credential] for that [protectionSpace]. Future removeHttpAuthCredential( - {required ProtectionSpace protectionSpace, - required HttpAuthCredential credential}) async { + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { Map args = {}; args.putIfAbsent("host", () => protectionSpace.host); args.putIfAbsent("protocol", () => protectionSpace.protocol); @@ -103,7 +96,7 @@ class HttpAuthCredentialDatabase { ///Removes all the HTTP auth credentials saved for that [protectionSpace]. Future removeHttpAuthCredentials( - {required ProtectionSpace protectionSpace}) async { + {required URLProtectionSpace protectionSpace}) async { Map args = {}; args.putIfAbsent("host", () => protectionSpace.host); args.putIfAbsent("protocol", () => protectionSpace.protocol); diff --git a/lib/src/in_app_browser/android/in_app_browser_options.dart b/lib/src/in_app_browser/android/in_app_browser_options.dart new file mode 100755 index 00000000..9b2d090a --- /dev/null +++ b/lib/src/in_app_browser/android/in_app_browser_options.dart @@ -0,0 +1,53 @@ +import '../../in_app_webview/android/in_app_webview_options.dart'; + +import '../in_app_browser_options.dart'; +import '../in_app_browser.dart'; + +///This class represents all the Android-only [InAppBrowser] options available. +class AndroidInAppBrowserOptions implements BrowserOptions, AndroidOptions { + ///Set to `true` if you want the title should be displayed. The default value is `false`. + bool hideTitleBar; + + ///Set the action bar's title. + String? toolbarTopFixedTitle; + + ///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`. + bool closeOnCannotGoBack; + + AndroidInAppBrowserOptions( + {this.hideTitleBar = false, + this.toolbarTopFixedTitle, + this.closeOnCannotGoBack = true}); + + @override + Map toMap() { + return { + "hideTitleBar": hideTitleBar, + "toolbarTopFixedTitle": toolbarTopFixedTitle, + "closeOnCannotGoBack": closeOnCannotGoBack, + }; + } + + static AndroidInAppBrowserOptions fromMap(Map map) { + AndroidInAppBrowserOptions options = AndroidInAppBrowserOptions(); + options.hideTitleBar = map["hideTitleBar"]; + options.toolbarTopFixedTitle = map["toolbarTopFixedTitle"]; + options.closeOnCannotGoBack = map["closeOnCannotGoBack"]; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + AndroidInAppBrowserOptions copy() { + return AndroidInAppBrowserOptions.fromMap(this.toMap()); + } +} \ No newline at end of file diff --git a/lib/src/in_app_browser/android/main.dart b/lib/src/in_app_browser/android/main.dart new file mode 100644 index 00000000..de948df1 --- /dev/null +++ b/lib/src/in_app_browser/android/main.dart @@ -0,0 +1 @@ +export 'in_app_browser_options.dart'; \ No newline at end of file diff --git a/lib/src/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart similarity index 92% rename from lib/src/in_app_browser.dart rename to lib/src/in_app_browser/in_app_browser.dart index 49f71d8a..02680ff8 100755 --- a/lib/src/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -3,12 +3,15 @@ import 'dart:collection'; import 'dart:typed_data'; import 'package:flutter/services.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'context_menu.dart'; -import 'in_app_webview_controller.dart'; -import 'webview_options.dart'; -import 'types.dart'; +import '../context_menu.dart'; +import '../types.dart'; +import '../_uuid_generator.dart'; + +import '../in_app_webview/in_app_webview_controller.dart'; +import '../in_app_webview/in_app_webview_options.dart'; + +import 'in_app_browser_options.dart'; ///This class uses the native WebView of the platform. ///The [webViewController] field can be used to access the [InAppWebViewController] API. @@ -30,12 +33,12 @@ class InAppBrowser { /// WebView Controller that can be used to access the [InAppWebViewController] API. late InAppWebViewController webViewController; - ///The window id of a [CreateWindowRequest.windowId]. + ///The window id of a [CreateWindowAction.windowId]. final int? windowId; /// InAppBrowser({this.windowId, this.initialUserScripts}) { - uuid = uuidGenerator.v4(); + uuid = UUID_GENERATOR.v4(); this._channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$uuid'); this._channel.setMethodCallHandler(handleMethod); @@ -59,32 +62,30 @@ class InAppBrowser { } } - ///Opens an [url] in a new [InAppBrowser] instance. + ///Opens an [urlRequest] in a new [InAppBrowser] instance. /// - ///[url]: The [url] to load. Call `encodeUriComponent()` on this if the [url] contains Unicode characters. The default value is `about:blank`. - /// - ///[headers]: The additional headers to be used in the HTTP request for this URL, specified as a map from name to value. + ///[urlRequest]: The [urlRequest] to load. /// ///[options]: Options for the [InAppBrowser]. - Future openUrl( - {required String url, - Map headers = const {}, + Future openUrlRequest( + {required URLRequest urlRequest, InAppBrowserClassOptions? options}) async { - assert(url.isNotEmpty); - this.throwIsAlreadyOpened(message: 'Cannot open $url!'); + this.throwIfAlreadyOpened(message: 'Cannot open $urlRequest!'); + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); Map args = {}; args.putIfAbsent('uuid', () => uuid); - args.putIfAbsent('url', () => url); - args.putIfAbsent('headers', () => headers); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); args.putIfAbsent('options', () => options?.toMap() ?? {}); args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); args.putIfAbsent('windowId', () => windowId); args.putIfAbsent('initialUserScripts', () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); - await _sharedChannel.invokeMethod('openUrl', args); + await _sharedChannel.invokeMethod('openUrlRequest', args); } - ///Opens the given [assetFilePath] file in a new [InAppBrowser] instance. The other arguments are the same of [InAppBrowser.openUrl]. + ///Opens the given [assetFilePath] file in a new [InAppBrowser] instance. + /// + ///[options]: Options for the [InAppBrowser]. /// ///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! /// @@ -110,7 +111,7 @@ class InAppBrowser { ///Example of a `main.dart` file: ///```dart ///... - ///inAppBrowser.openFile("assets/index.html"); + ///inAppBrowser.openFile(assetFilePath: "assets/index.html"); ///... ///``` /// @@ -119,15 +120,13 @@ class InAppBrowser { ///[options]: Options for the [InAppBrowser]. Future openFile( {required String assetFilePath, - Map headers = const {}, InAppBrowserClassOptions? options}) async { + this.throwIfAlreadyOpened(message: 'Cannot open $assetFilePath!'); assert(assetFilePath.isNotEmpty); - this.throwIsAlreadyOpened(message: 'Cannot open $assetFilePath!'); Map args = {}; args.putIfAbsent('uuid', () => uuid); - args.putIfAbsent('url', () => assetFilePath); - args.putIfAbsent('headers', () => headers); + args.putIfAbsent('assetFilePath', () => assetFilePath); args.putIfAbsent('options', () => options?.toMap() ?? {}); args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); args.putIfAbsent('windowId', () => windowId); @@ -148,17 +147,19 @@ class InAppBrowser { {required String data, String mimeType = "text/html", String encoding = "utf8", - String baseUrl = "about:blank", - String androidHistoryUrl = "about:blank", + Uri? baseUrl, + Uri? androidHistoryUrl, InAppBrowserClassOptions? options}) async { + this.throwIfAlreadyOpened(message: 'Cannot open data!'); + Map args = {}; args.putIfAbsent('uuid', () => uuid); args.putIfAbsent('options', () => options?.toMap() ?? {}); args.putIfAbsent('data', () => data); args.putIfAbsent('mimeType', () => mimeType); args.putIfAbsent('encoding', () => encoding); - args.putIfAbsent('baseUrl', () => baseUrl); - args.putIfAbsent('historyUrl', () => androidHistoryUrl); + args.putIfAbsent('baseUrl', () => baseUrl ?? Uri.parse("about:blank")); + args.putIfAbsent('historyUrl', () => androidHistoryUrl ?? Uri.parse("about:blank")); args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); args.putIfAbsent('windowId', () => windowId); args.putIfAbsent('initialUserScripts', () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); @@ -166,16 +167,16 @@ class InAppBrowser { } ///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 openWithSystemBrowser({required String url}) async { - assert(url.isNotEmpty); + static Future openWithSystemBrowser({required Uri url}) async { + assert(url.toString().isNotEmpty); Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); return await _sharedChannel.invokeMethod('openWithSystemBrowser', args); } ///Displays an [InAppBrowser] window that was opened hidden. Calling this has no effect if the [InAppBrowser] was already visible. Future show() async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; args.putIfAbsent('uuid', () => uuid); await _channel.invokeMethod('show', args); @@ -183,28 +184,28 @@ class InAppBrowser { ///Hides the [InAppBrowser] window. Calling this has no effect if the [InAppBrowser] was already hidden. Future hide() async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; await _channel.invokeMethod('hide', args); } ///Closes the [InAppBrowser] window. Future close() async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; await _channel.invokeMethod('close', args); } ///Check if the Web View of the [InAppBrowser] instance is hidden. Future isHidden() async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; return await _channel.invokeMethod('isHidden', args); } ///Sets the [InAppBrowser] options with the new [options] and evaluates them. Future setOptions({required InAppBrowserClassOptions options}) async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; args.putIfAbsent('options', () => options.toMap()); @@ -213,7 +214,7 @@ class InAppBrowser { ///Gets the current [InAppBrowser] options. Returns `null` if it wasn't able to get them. Future getOptions() async { - this.throwIsNotOpened(); + this.throwIfNotOpened(); Map args = {}; Map? options = @@ -242,21 +243,21 @@ class InAppBrowser { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview - void onLoadStart(String? url) {} + void onLoadStart(Uri? url) {} ///Event fired when the [InAppBrowser] finishes loading an [url]. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onPageFinished(android.webkit.WebView,%20java.lang.String) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview - void onLoadStop(String? url) {} + void onLoadStop(Uri? url) {} ///Event fired when the [InAppBrowser] encounters an error loading an [url]. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedError(android.webkit.WebView,%20int,%20java.lang.String,%20java.lang.String) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455623-webview - void onLoadError(String? url, int code, String message) {} + void onLoadError(Uri? url, int code, String message) {} ///Event fired when the [InAppBrowser] main page receives an HTTP error. /// @@ -271,7 +272,7 @@ class InAppBrowser { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedHttpError(android.webkit.WebView,%20android.webkit.WebResourceRequest,%20android.webkit.WebResourceResponse) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview - void onLoadHttpError(String? url, int statusCode, String description) {} + void onLoadHttpError(Uri? url, int statusCode, String description) {} ///Event fired when the current [progress] (range 0-100) of loading a page is changed. /// @@ -291,15 +292,15 @@ class InAppBrowser { /// ///Also, on Android, this method is not called for POST requests. /// - ///[shouldOverrideUrlLoadingRequest] represents the navigation request. + ///[navigationAction] represents an object that contains information about an action that causes navigation to occur. /// ///**NOTE**: In order to be able to listen this event, you need to set [InAppWebViewOptions.useShouldOverrideUrlLoading] option to `true`. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#shouldOverrideUrlLoading(android.webkit.WebView,%20java.lang.String) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455641-webview - Future? shouldOverrideUrlLoading( - ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest) {} + Future? shouldOverrideUrlLoading( + NavigationAction navigationAction) {} ///Event fired when the [InAppBrowser] webview loads a resource. /// @@ -326,7 +327,7 @@ class InAppBrowser { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#setDownloadListener(android.webkit.DownloadListener) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview - void onDownloadStart(String url) {} + void onDownloadStart(Uri url) {} ///Event fired when the [InAppBrowser] webview finds the `custom-scheme` while loading a resource. Here you can handle the url request and return a [CustomSchemeResponse] to load a specific resource encoded to `base64`. /// @@ -336,7 +337,7 @@ class InAppBrowser { /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkurlschemehandler Future? onLoadResourceCustomScheme( - String scheme, String url) {} + Uri url) {} ///Event fired when the [InAppBrowser] webview requests the host application to create a new window, ///for example when trying to open a link with `target="_blank"` or when `window.open()` is called by JavaScript side. @@ -344,7 +345,7 @@ class InAppBrowser { ///If the host application chooses not to honor the request, it should return `false` from this method. ///The default implementation of this method does nothing and hence returns `false`. /// - ///[createWindowRequest] represents the request. + ///[createWindowAction] represents the request. /// ///**NOTE**: to allow JavaScript to open windows, you need to set [InAppWebViewOptions.javaScriptCanOpenWindowsAutomatically] option to `true`. /// @@ -368,7 +369,7 @@ class InAppBrowser { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebChromeClient#onCreateWindow(android.webkit.WebView,%20boolean,%20boolean,%20android.os.Message) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkuidelegate/1536907-webview - Future? onCreateWindow(CreateWindowRequest createWindowRequest) {} + Future? onCreateWindow(CreateWindowAction createWindowAction) {} ///Event fired when the host application should close the given WebView and remove it from the view system if necessary. ///At this point, WebCore has stopped any loading in this window and has removed any cross-scripting ability in javascript. @@ -435,7 +436,7 @@ class InAppBrowser { /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview Future? onReceivedServerTrustAuthRequest( - ServerTrustChallenge challenge) {} + URLAuthenticationChallenge challenge) {} ///Notify the host application to handle an SSL client certificate request. ///Webview stores the response in memory (for the life of the application) if [ClientCertResponseAction.PROCEED] or [ClientCertResponseAction.CANCEL] @@ -448,7 +449,7 @@ class InAppBrowser { /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455638-webview Future? onReceivedClientCertRequest( - ClientCertChallenge challenge) {} + URLAuthenticationChallenge challenge) {} ///Event fired as find-on-page operations progress. ///The listener may be notified multiple times while the operation is underway, and the numberOfMatches value should not be considered final unless [isDoneCounting] is true. @@ -505,14 +506,14 @@ class InAppBrowser { ///[androidIsReload] indicates if this url is being reloaded. Available only on Android. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#doUpdateVisitedHistory(android.webkit.WebView,%20java.lang.String,%20boolean) - void onUpdateVisitedHistory(String? url, bool? androidIsReload) {} + void onUpdateVisitedHistory(Uri? url, bool? androidIsReload) {} ///Event fired when `window.print()` is called from JavaScript side. /// ///[url] represents the url on which is called. /// ///**NOTE**: available on Android 21+. - void onPrint(String? url) {} + void onPrint(Uri? url) {} ///Event fired when an HTML element of the webview has been clicked and held. /// @@ -547,7 +548,7 @@ class InAppBrowser { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onPageCommitVisible(android.webkit.WebView,%20java.lang.String) /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455635-webview - void onPageCommitVisible(String? url) {} + void onPageCommitVisible(Uri? url) {} ///Event fired when a change in the document title occurred. /// @@ -567,7 +568,7 @@ class InAppBrowser { /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onSafeBrowsingHit(android.webkit.WebView,%20android.webkit.WebResourceRequest,%20int,%20android.webkit.SafeBrowsingResponse) Future? androidOnSafeBrowsingHit( - String url, SafeBrowsingThreat? threatType) {} + Uri url, SafeBrowsingThreat? threatType) {} ///Event fired when the WebView is requesting permission to access the specified resources and the permission currently isn't granted or denied. /// @@ -640,7 +641,7 @@ class InAppBrowser { /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewRenderProcessClient#onRenderProcessUnresponsive(android.webkit.WebView,%20android.webkit.WebViewRenderProcess) Future? - androidOnRenderProcessUnresponsive(String? url) {} + androidOnRenderProcessUnresponsive(Uri? url) {} ///Event called once when an unresponsive renderer currently associated with the WebView becomes responsive. /// @@ -654,7 +655,7 @@ class InAppBrowser { /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewRenderProcessClient#onRenderProcessResponsive(android.webkit.WebView,%20android.webkit.WebViewRenderProcess) Future? - androidOnRenderProcessResponsive(String? url) {} + androidOnRenderProcessResponsive(Uri? url) {} ///Event fired when the given WebView's render process has exited. ///The application's implementation of this callback should only attempt to clean up the WebView. @@ -673,7 +674,7 @@ class InAppBrowser { /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebViewClient#onFormResubmission(android.webkit.WebView,%20android.os.Message,%20android.os.Message) Future? - androidOnFormResubmission(String? url) {} + androidOnFormResubmission(Uri? url) {} ///Event fired when the scale applied to the WebView has changed. /// @@ -704,7 +705,7 @@ class InAppBrowser { ///**NOTE**: available only on Android. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebChromeClient#onReceivedTouchIconUrl(android.webkit.WebView,%20java.lang.String,%20boolean) - void androidOnReceivedTouchIconUrl(String url, bool precomposed) {} + void androidOnReceivedTouchIconUrl(Uri url, bool precomposed) {} ///Event fired when the client should display a dialog to confirm navigation away from the current page. ///This is the result of the `onbeforeunload` javascript event. @@ -753,7 +754,7 @@ class InAppBrowser { /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview Future? - iosOnNavigationResponse(IOSNavigationResponse navigationResponse) {} + iosOnNavigationResponse(IOSWKNavigationResponse navigationResponse) {} ///Called when a web view asks whether to continue with a connection that uses a deprecated version of TLS (v1.0 and v1.1). /// @@ -765,7 +766,7 @@ class InAppBrowser { Future? iosShouldAllowDeprecatedTLS(URLAuthenticationChallenge challenge) {} - void throwIsAlreadyOpened({String message = ''}) { + void throwIfAlreadyOpened({String message = ''}) { if (this.isOpened()) { throw Exception([ 'Error: ${(message.isEmpty) ? '' : message + ' '}The browser is already opened.' @@ -773,7 +774,7 @@ class InAppBrowser { } } - void throwIsNotOpened({String message = ''}) { + void throwIfNotOpened({String message = ''}) { if (!this.isOpened()) { throw Exception([ 'Error: ${(message.isEmpty) ? '' : message + ' '}The browser is not opened.' diff --git a/lib/src/in_app_browser/in_app_browser_options.dart b/lib/src/in_app_browser/in_app_browser_options.dart new file mode 100755 index 00000000..2e9ce7e9 --- /dev/null +++ b/lib/src/in_app_browser/in_app_browser_options.dart @@ -0,0 +1,180 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../util.dart'; + +import '../in_app_webview/in_app_webview_options.dart'; + +import 'android/in_app_browser_options.dart'; +import '../in_app_webview/android/in_app_webview_options.dart'; + +import 'ios/in_app_browser_options.dart'; +import '../in_app_webview/ios/in_app_webview_options.dart'; + +class BrowserOptions { + Map toMap() { + return {}; + } + + static BrowserOptions fromMap(Map map) { + return new BrowserOptions(); + } + + BrowserOptions copy() { + return BrowserOptions.fromMap(this.toMap()); + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + + +///Class that represents the options that can be used for an [InAppBrowser] WebView. +class InAppBrowserClassOptions { + ///Cross-platform options. + InAppBrowserOptions? crossPlatform; + + ///Android-specific options. + AndroidInAppBrowserOptions? android; + + ///iOS-specific options. + IOSInAppBrowserOptions? ios; + + ///WebView options. + InAppWebViewGroupOptions? inAppWebViewGroupOptions; + + InAppBrowserClassOptions( + {this.crossPlatform, + this.android, + this.ios, + this.inAppWebViewGroupOptions}) { + this.crossPlatform = this.crossPlatform ?? InAppBrowserOptions(); + this.android = this.android ?? AndroidInAppBrowserOptions(); + this.ios = this.ios ?? IOSInAppBrowserOptions(); + this.inAppWebViewGroupOptions = + this.inAppWebViewGroupOptions ?? InAppWebViewGroupOptions(); + } + + Map toMap() { + Map options = {}; + + options.addAll(this.crossPlatform?.toMap() ?? {}); + options.addAll(this.inAppWebViewGroupOptions?.crossPlatform.toMap() ?? {}); + if (defaultTargetPlatform == TargetPlatform.android) { + options.addAll(this.android?.toMap() ?? {}); + options.addAll(this.inAppWebViewGroupOptions?.android.toMap() ?? {}); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + options.addAll(this.ios?.toMap() ?? {}); + options.addAll(this.inAppWebViewGroupOptions?.ios.toMap() ?? {}); + } + + return options; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + static InAppBrowserClassOptions fromMap(Map options) { + InAppBrowserClassOptions inAppBrowserClassOptions = + InAppBrowserClassOptions(); + + inAppBrowserClassOptions.crossPlatform = + InAppBrowserOptions.fromMap(options); + inAppBrowserClassOptions.inAppWebViewGroupOptions = + InAppWebViewGroupOptions(); + inAppBrowserClassOptions.inAppWebViewGroupOptions!.crossPlatform = + InAppWebViewOptions.fromMap(options); + if (defaultTargetPlatform == TargetPlatform.android) { + inAppBrowserClassOptions.android = + AndroidInAppBrowserOptions.fromMap(options); + inAppBrowserClassOptions.inAppWebViewGroupOptions!.android = + AndroidInAppWebViewOptions.fromMap(options); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + inAppBrowserClassOptions.ios = IOSInAppBrowserOptions.fromMap(options); + inAppBrowserClassOptions.inAppWebViewGroupOptions!.ios = + IOSInAppWebViewOptions.fromMap(options); + } + + return inAppBrowserClassOptions; + } + + InAppBrowserClassOptions copy() { + return InAppBrowserClassOptions.fromMap(this.toMap()); + } +} + +///This class represents all the cross-platform [InAppBrowser] options available. +class InAppBrowserOptions + implements BrowserOptions, AndroidOptions, IosOptions { + ///Set to `true` to create the browser and load the page, but not show it. Omit or set to `false` to have the browser open and load normally. + ///The default value is `false`. + bool hidden; + + ///Set to `true` to hide the toolbar at the top of the WebView. The default value is `false`. + bool hideToolbarTop; + + ///Set the custom background color of the toolbar at the top. + Color? toolbarTopBackgroundColor; + + ///Set to `true` to hide the url bar on the toolbar at the top. The default value is `false`. + bool hideUrlBar; + + ///Set to `true` to hide the progress bar when the WebView is loading a page. The default value is `false`. + bool hideProgressBar; + + InAppBrowserOptions( + {this.hidden = false, + this.hideToolbarTop = false, + this.toolbarTopBackgroundColor, + this.hideUrlBar = false, + this.hideProgressBar = false}); + + @override + Map toMap() { + return { + "hidden": hidden, + "hideToolbarTop": hideToolbarTop, + "toolbarTopBackgroundColor": toolbarTopBackgroundColor?.toHex(), + "hideUrlBar": hideUrlBar, + "hideProgressBar": hideProgressBar + }; + } + + static InAppBrowserOptions fromMap(Map map) { + InAppBrowserOptions options = InAppBrowserOptions(); + options.hidden = map["hidden"]; + options.hideToolbarTop = map["hideToolbarTop"]; + options.toolbarTopBackgroundColor = UtilColor.fromHex(map["toolbarTopBackgroundColor"]); + options.hideUrlBar = map["hideUrlBar"]; + options.hideProgressBar = map["hideProgressBar"]; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + InAppBrowserOptions copy() { + return InAppBrowserOptions.fromMap(this.toMap()); + } +} diff --git a/lib/src/in_app_browser/ios/in_app_browser_options.dart b/lib/src/in_app_browser/ios/in_app_browser_options.dart new file mode 100755 index 00000000..08648bf9 --- /dev/null +++ b/lib/src/in_app_browser/ios/in_app_browser_options.dart @@ -0,0 +1,105 @@ +import 'dart:ui'; + +import '../../in_app_webview/ios/in_app_webview_options.dart'; + +import '../in_app_browser_options.dart'; +import '../in_app_browser.dart'; + +import '../../types.dart'; +import '../../util.dart'; + +///This class represents all the iOS-only [InAppBrowser] options available. +class IOSInAppBrowserOptions implements BrowserOptions, IosOptions { + ///Set to `true` to set the toolbar at the top translucent. The default value is `true`. + bool toolbarTopTranslucent; + + ///Set the tint color to apply to the navigation bar background. + Color? toolbarTopBarTintColor; + + ///Set the tint color to apply to the navigation items and bar button items. + Color? toolbarTopTintColor; + + ///Set to `true` to hide the toolbar at the bottom of the WebView. The default value is `false`. + bool hideToolbarBottom; + + ///Set the custom background color of the toolbar at the bottom. + Color? toolbarBottomBackgroundColor; + + ///Set the tint color to apply to the bar button items. + Color? toolbarBottomTintColor; + + ///Set to `true` to set the toolbar at the bottom translucent. The default value is `true`. + bool toolbarBottomTranslucent; + + ///Set the custom text for the close button. + String? closeButtonCaption; + + ///Set the custom color for the close button. + Color? closeButtonColor; + + ///Set the custom modal presentation style when presenting the WebView. The default value is [IOSUIModalPresentationStyle.FULL_SCREEN]. + IOSUIModalPresentationStyle presentationStyle; + + ///Set to the custom transition style when presenting the WebView. The default value is [IOSUIModalTransitionStyle.COVER_VERTICAL]. + IOSUIModalTransitionStyle transitionStyle; + + IOSInAppBrowserOptions( + {this.toolbarTopTranslucent = true, + this.toolbarTopTintColor, + this.hideToolbarBottom = false, + this.toolbarBottomBackgroundColor, + this.toolbarBottomTintColor, + this.toolbarBottomTranslucent = true, + this.closeButtonCaption, + this.closeButtonColor, + this.presentationStyle = IOSUIModalPresentationStyle.FULL_SCREEN, + this.transitionStyle = IOSUIModalTransitionStyle.COVER_VERTICAL}); + + @override + Map toMap() { + return { + "toolbarTopTranslucent": toolbarTopTranslucent, + "toolbarTopTintColor": toolbarTopTintColor?.toHex(), + "hideToolbarBottom": hideToolbarBottom, + "toolbarBottomBackgroundColor": toolbarBottomBackgroundColor?.toHex(), + "toolbarBottomTintColor": toolbarBottomTintColor?.toHex(), + "toolbarBottomTranslucent": toolbarBottomTranslucent, + "closeButtonCaption": closeButtonCaption, + "closeButtonColor": closeButtonColor?.toHex(), + "presentationStyle": presentationStyle.toValue(), + "transitionStyle": transitionStyle.toValue(), + }; + } + + static IOSInAppBrowserOptions fromMap(Map map) { + IOSInAppBrowserOptions options = IOSInAppBrowserOptions(); + options.toolbarTopTranslucent = map["toolbarTopTranslucent"]; + options.toolbarTopTintColor = UtilColor.fromHex(map["toolbarTopTintColor"]); + options.hideToolbarBottom = map["hideToolbarBottom"]; + options.toolbarBottomBackgroundColor = UtilColor.fromHex(map["toolbarBottomBackgroundColor"]); + options.toolbarBottomTintColor = UtilColor.fromHex(map["toolbarBottomTintColor"]); + options.toolbarBottomTranslucent = map["toolbarBottomTranslucent"]; + options.closeButtonCaption = map["closeButtonCaption"]; + options.closeButtonColor = UtilColor.fromHex(map["closeButtonColor"]); + options.presentationStyle = + IOSUIModalPresentationStyle.fromValue(map["presentationStyle"])!; + options.transitionStyle = + IOSUIModalTransitionStyle.fromValue(map["transitionStyle"])!; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + IOSInAppBrowserOptions copy() { + return IOSInAppBrowserOptions.fromMap(this.toMap()); + } +} diff --git a/lib/src/in_app_browser/ios/main.dart b/lib/src/in_app_browser/ios/main.dart new file mode 100644 index 00000000..de948df1 --- /dev/null +++ b/lib/src/in_app_browser/ios/main.dart @@ -0,0 +1 @@ +export 'in_app_browser_options.dart'; \ No newline at end of file diff --git a/lib/src/in_app_browser/main.dart b/lib/src/in_app_browser/main.dart new file mode 100644 index 00000000..96ec1eee --- /dev/null +++ b/lib/src/in_app_browser/main.dart @@ -0,0 +1,4 @@ +export 'in_app_browser.dart'; +export 'in_app_browser_options.dart'; +export 'android/main.dart'; +export 'ios/main.dart'; \ No newline at end of file diff --git a/lib/src/in_app_webview/_static_channel.dart b/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 00000000..eab9a19e --- /dev/null +++ b/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,3 @@ +import 'package:flutter/services.dart'; + +const IN_APP_WEBVIEW_STATIC_CHANNEL = const MethodChannel('com.pichillilorenzo/flutter_inappwebview_static'); \ No newline at end of file diff --git a/lib/src/in_app_webview/android/in_app_webview_controller.dart b/lib/src/in_app_webview/android/in_app_webview_controller.dart new file mode 100644 index 00000000..063974b7 --- /dev/null +++ b/lib/src/in_app_webview/android/in_app_webview_controller.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:core'; + +import 'package:flutter/services.dart'; + +import '../_static_channel.dart'; + +import '../../types.dart'; + +///Class represents the Android controller that contains only android-specific methods for the WebView. +class AndroidInAppWebViewController { + + late MethodChannel _channel; + static MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + AndroidInAppWebViewController({required MethodChannel channel}) { + this._channel = channel; + } + + ///Starts Safe Browsing initialization. + /// + ///URL loads are not guaranteed to be protected by Safe Browsing until after the this method returns true. + ///Safe Browsing is not fully supported on all devices. For those devices this method will returns false. + /// + ///This should not be called if Safe Browsing has been disabled by manifest tag + ///or [AndroidInAppWebViewOptions.safeBrowsingEnabled]. This prepares resources used for Safe Browsing. + /// + ///**NOTE**: available only on Android 27+. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#startSafeBrowsing(android.content.Context,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E) + Future startSafeBrowsing() async { + Map args = {}; + return await _channel.invokeMethod('startSafeBrowsing', args); + } + + ///Clears the SSL preferences table stored in response to proceeding with SSL certificate errors. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearSslPreferences() + Future clearSslPreferences() async { + Map args = {}; + await _channel.invokeMethod('clearSslPreferences', args); + } + + ///Does a best-effort attempt to pause any processing that can be paused safely, such as animations and geolocation. Note that this call does not pause JavaScript. + ///To pause JavaScript globally, use [pauseTimers()]. To resume WebView, call [resume()]. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#onPause() + Future pause() async { + Map args = {}; + await _channel.invokeMethod('pause', args); + } + + ///Resumes a WebView after a previous call to [pause()]. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#onResume() + Future resume() async { + Map args = {}; + await _channel.invokeMethod('resume', args); + } + + ///Gets the URL that was originally requested for the current page. + ///This is not always the same as the URL passed to [InAppWebView.onLoadStarted] because although the load for that URL has begun, + ///the current page may not have changed. Also, there may have been redirects resulting in a different URL to that originally requested. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getOriginalUrl() + Future getOriginalUrl() async { + Map args = {}; + String? url = await _channel.invokeMethod('getOriginalUrl', args); + return url != null ? Uri.parse(url) : null; + } + + ///Scrolls the contents of this WebView down by half the page size. + ///Returns `true` if the page was scrolled. + /// + ///[bottom] `true` to jump to bottom of page. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#pageDown(boolean) + Future pageDown({required bool bottom}) async { + Map args = {}; + args.putIfAbsent("bottom", () => bottom); + return await _channel.invokeMethod('pageDown', args); + } + + ///Scrolls the contents of this WebView up by half the view size. + ///Returns `true` if the page was scrolled. + /// + ///[bottom] `true` to jump to the top of the page. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#pageUp(boolean) + Future pageUp({required bool top}) async { + Map args = {}; + args.putIfAbsent("top", () => top); + return await _channel.invokeMethod('pageUp', args); + } + + ///Performs zoom in in this WebView. + ///Returns `true` if zoom in succeeds, `false` if no zoom changes. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#zoomIn() + Future zoomIn() async { + Map args = {}; + return await _channel.invokeMethod('zoomIn', args); + } + + ///Performs zoom out in this WebView. + ///Returns `true` if zoom out succeeds, `false` if no zoom changes. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#zoomOut() + Future zoomOut() async { + Map args = {}; + return await _channel.invokeMethod('zoomOut', args); + } + + ///Clears the internal back/forward list. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearHistory() + Future clearHistory() async { + Map args = {}; + return await _channel.invokeMethod('clearHistory', args); + } + + ///Clears the client certificate preferences stored in response to proceeding/cancelling client cert requests. + ///Note that WebView automatically clears these preferences when the system keychain is updated. + ///The preferences are shared by all the WebViews that are created by the embedder application. + /// + ///**NOTE**: On iOS certificate-based credentials are never stored permanently. + /// + ///**NOTE**: available on Android 21+. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearClientCertPreferences(java.lang.Runnable) + static Future clearClientCertPreferences() async { + Map args = {}; + await _staticChannel + .invokeMethod('clearClientCertPreferences', args); + } + + ///Returns a URL pointing to the privacy policy for Safe Browsing reporting. + /// + ///**NOTE**: available only on Android 27+. + /// + ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl() + static Future getSafeBrowsingPrivacyPolicyUrl() async { + Map args = {}; + String? url = await _staticChannel + .invokeMethod('getSafeBrowsingPrivacyPolicyUrl', args); + return url != null ? Uri.parse(url) : null; + } + + ///Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks. The list is global for all the WebViews. + /// + /// Each rule should take one of these: + ///| Rule | Example | Matches Subdomain | + ///| -- | -- | -- | + ///| HOSTNAME | example.com | Yes | + ///| .HOSTNAME | .example.com | No | + ///| IPV4_LITERAL | 192.168.1.1 | No | + ///| IPV6_LITERAL_WITH_BRACKETS | [10:20:30:40:50:60:70:80] | No | + /// + ///All other rules, including wildcards, are invalid. The correct syntax for hosts is defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). + /// + ///[hosts] represents the list of hosts. This value must never be `null`. + /// + ///**NOTE**: available only on Android 27+. + /// + ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl() + static Future setSafeBrowsingWhitelist( + {required List hosts}) async { + Map args = {}; + args.putIfAbsent('hosts', () => hosts); + return await _staticChannel + .invokeMethod('setSafeBrowsingWhitelist', args); + } + + ///If WebView has already been loaded into the current process this method will return the package that was used to load it. + ///Otherwise, the package that would be used if the WebView was loaded right now will be returned; + ///this does not cause WebView to be loaded, so this information may become outdated at any time. + ///The WebView package changes either when the current WebView package is updated, disabled, or uninstalled. + ///It can also be changed through a Developer Setting. If the WebView package changes, any app process that + ///has loaded WebView will be killed. + ///The next time the app starts and loads WebView it will use the new WebView package instead. + /// + ///**NOTE**: available only on Android 26+. + /// + ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getCurrentWebViewPackage(android.content.Context) + static Future getCurrentWebViewPackage() async { + Map args = {}; + Map? packageInfo = (await _staticChannel + .invokeMethod('getCurrentWebViewPackage', args)) + ?.cast(); + return AndroidWebViewPackageInfo.fromMap(packageInfo); + } + + ///Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. + ///This flag can be enabled in order to facilitate debugging of web layouts and JavaScript code running inside WebViews. + ///Please refer to WebView documentation for the debugging guide. The default is `false`. + /// + ///[debuggingEnabled] whether to enable web contents debugging. + /// + ///**NOTE**: available only on Android 19+. + /// + ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#setWebContentsDebuggingEnabled(boolean) + static Future setWebContentsDebuggingEnabled(bool debuggingEnabled) async { + Map args = {}; + args.putIfAbsent('debuggingEnabled', () => debuggingEnabled); + return await _staticChannel + .invokeMethod('setWebContentsDebuggingEnabled', args); + } +} + diff --git a/lib/src/in_app_webview/android/in_app_webview_options.dart b/lib/src/in_app_webview/android/in_app_webview_options.dart new file mode 100755 index 00000000..caf7602a --- /dev/null +++ b/lib/src/in_app_webview/android/in_app_webview_options.dart @@ -0,0 +1,410 @@ +import '../../types.dart'; + +import '../../in_app_browser/in_app_browser_options.dart'; + +import '../in_app_webview_options.dart'; +import '../webview.dart'; + +class AndroidOptions {} + +///This class represents all the Android-only WebView options available. +class AndroidInAppWebViewOptions + implements WebViewOptions, BrowserOptions, AndroidOptions { + ///Sets the text zoom of the page in percent. The default value is `100`. + int textZoom; + + ///Set to `true` to have the session cookie cache cleared before the new window is opened. + bool clearSessionCache; + + ///Set to `true` if the WebView should use its built-in zoom mechanisms. The default value is `true`. + bool builtInZoomControls; + + ///Set to `true` if the WebView should display on-screen zoom controls when using the built-in zoom mechanisms. The default value is `false`. + bool displayZoomControls; + + ///Set to `true` if you want the database storage API is enabled. The default value is `true`. + bool databaseEnabled; + + ///Set to `true` if you want the DOM storage API is enabled. The default value is `true`. + bool domStorageEnabled; + + ///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`. + bool useWideViewPort; + + ///Sets whether Safe Browsing is enabled. Safe Browsing allows WebView to protect against malware and phishing attacks by verifying the links. + ///Safe Browsing is enabled by default for devices which support it. + /// + ///**NOTE**: available on Android 26+. + bool safeBrowsingEnabled; + + ///Configures the WebView's behavior when a secure origin attempts to load a resource from an insecure origin. + /// + ///**NOTE**: available on Android 21+. + AndroidMixedContentMode? mixedContentMode; + + ///Enables or disables content URL access within WebView. Content URL access allows WebView to load content from a content provider installed in the system. The default value is `true`. + bool allowContentAccess; + + ///Enables or disables file access within WebView. Note that this enables or disables file system access only. + ///Assets and resources are still accessible using `file:///android_asset` and `file:///android_res`. The default value is `true`. + bool allowFileAccess; + + ///Sets the path to the Application Caches files. In order for the Application Caches API to be enabled, this option must be set a path to which the application can write. + ///This option is used one time: repeated calls are ignored. + String? appCachePath; + + ///Sets whether the WebView should not load image resources from the network (resources accessed via http and https URI schemes). The default value is `false`. + bool blockNetworkImage; + + ///Sets whether the WebView should not load resources from the network. The default value is `false`. + bool blockNetworkLoads; + + ///Overrides the way the cache is used. The way the cache is used is based on the navigation type. For a normal page load, the cache is checked and content is re-validated as needed. + ///When navigating back, content is not revalidated, instead the content is just retrieved from the cache. The default value is [AndroidCacheMode.LOAD_DEFAULT]. + AndroidCacheMode? cacheMode; + + ///Sets the cursive font family name. The default value is `"cursive"`. + String cursiveFontFamily; + + ///Sets the default fixed font size. The default value is `16`. + int defaultFixedFontSize; + + ///Sets the default font size. The default value is `16`. + int defaultFontSize; + + ///Sets the default text encoding name to use when decoding html pages. The default value is `"UTF-8"`. + String defaultTextEncodingName; + + ///Disables the action mode menu items according to menuItems flag. + /// + ///**NOTE**: available on Android 24+. + AndroidActionModeMenuItem? disabledActionModeMenuItems; + + ///Sets the fantasy font family name. The default value is `"fantasy"`. + String fantasyFontFamily; + + ///Sets the fixed font family name. The default value is `"monospace"`. + String fixedFontFamily; + + ///Set the force dark mode for this WebView. The default value is [AndroidForceDark.FORCE_DARK_OFF]. + /// + ///**NOTE**: available on Android 29+. + AndroidForceDark? forceDark; + + ///Sets whether Geolocation API is enabled. The default value is `true`. + bool geolocationEnabled; + + ///Sets the underlying layout algorithm. This will cause a re-layout of the WebView. + AndroidLayoutAlgorithm? layoutAlgorithm; + + ///Sets whether the WebView loads pages in overview mode, that is, zooms out the content to fit on screen by width. + ///This setting is taken into account when the content width is greater than the width of the WebView control, for example, when [useWideViewPort] is enabled. + ///The default value is `false`. + bool loadWithOverviewMode; + + ///Sets whether the WebView should load image resources. Note that this method controls loading of all images, including those embedded using the data URI scheme. + ///Note that if the value of this setting is changed from false to true, all images resources referenced by content currently displayed by the WebView are loaded automatically. + ///The default value is `true`. + bool loadsImagesAutomatically; + + ///Sets the minimum logical font size. The default is `8`. + int minimumLogicalFontSize; + + ///Sets the initial scale for this WebView. 0 means default. The behavior for the default scale depends on the state of [useWideViewPort] and [loadWithOverviewMode]. + ///If the content fits into the WebView control by width, then the zoom is set to 100%. For wide content, the behavior depends on the state of [loadWithOverviewMode]. + ///If its value is true, the content will be zoomed out to be fit by width into the WebView control, otherwise not. + ///If initial scale is greater than 0, WebView starts with this value as initial scale. + ///Please note that unlike the scale properties in the viewport meta tag, this method doesn't take the screen density into account. + ///The default is `0`. + int initialScale; + + ///Tells the WebView whether it needs to set a node. The default value is `true`. + bool needInitialFocus; + + ///Sets whether this WebView should raster tiles when it is offscreen but attached to a window. + ///Turning this on can avoid rendering artifacts when animating an offscreen WebView on-screen. + ///Offscreen WebViews in this mode use more memory. The default value is `false`. + /// + ///**NOTE**: available on Android 23+. + bool offscreenPreRaster; + + ///Sets the sans-serif font family name. The default value is `"sans-serif"`. + String sansSerifFontFamily; + + ///Sets the serif font family name. The default value is `"sans-serif"`. + String serifFontFamily; + + ///Sets the standard font family name. The default value is `"sans-serif"`. + String standardFontFamily; + + ///Sets whether the WebView should save form data. In Android O, the platform has implemented a fully functional Autofill feature to store form data. + ///Therefore, the Webview form data save feature is disabled. Note that the feature will continue to be supported on older versions of Android as before. + ///The default value is `true`. + bool saveFormData; + + ///Boolean value to enable third party cookies in the WebView. + ///Used on Android Lollipop and above only as third party cookies are enabled by default on Android Kitkat and below and on iOS. + ///The default value is `true`. + /// + ///**NOTE**: available on Android 21+. + bool thirdPartyCookiesEnabled; + + ///Boolean value to enable Hardware Acceleration in the WebView. + ///The default value is `true`. + bool hardwareAcceleration; + + ///Sets whether the WebView supports multiple windows. + ///If set to `true`, [WebView.onCreateWindow] event must be implemented by the host application. The default value is `false`. + bool supportMultipleWindows; + + ///Regular expression used by [WebView.shouldOverrideUrlLoading] event to cancel navigation requests for frames that are not the main frame. + ///If the url request of a subframe matches the regular expression, then the request of that subframe is canceled. + String? regexToCancelSubFramesLoading; + + ///Set to `true` to enable Flutter's new Hybrid Composition. The default value is `false`. + ///Hybrid Composition is supported starting with Flutter v1.20+. + /// + ///**NOTE**: It is recommended to use Hybrid Composition only on Android 10+ for a release app, + ///as it can cause framerate drops on animations in Android 9 and lower (see [Hybrid-Composition#performance](https://github.com/flutter/flutter/wiki/Hybrid-Composition#performance)). + bool useHybridComposition; + + ///Set to `true` to be able to listen at the [WebView.androidShouldInterceptRequest] event. The default value is `false`. + bool useShouldInterceptRequest; + + ///Set to `true` to be able to listen at the [WebView.androidOnRenderProcessGone] event. The default value is `false`. + bool useOnRenderProcessGone; + + ///Sets the WebView's over-scroll mode. + ///Setting the over-scroll mode of a WebView will have an effect only if the WebView is capable of scrolling. + ///The default value is [AndroidOverScrollMode.OVER_SCROLL_IF_CONTENT_SCROLLS]. + AndroidOverScrollMode? overScrollMode; + + ///Informs WebView of the network state. + ///This is used to set the JavaScript property `window.navigator.isOnline` and generates the online/offline event as specified in HTML5, sec. 5.7.7. + bool? networkAvailable; + + ///Specifies the style of the scrollbars. The scrollbars can be overlaid or inset. + ///When inset, they add to the padding of the view. And the scrollbars can be drawn inside the padding area or on the edge of the view. + ///For example, if a view has a background drawable and you want to draw the scrollbars inside the padding specified by the drawable, + ///you can use SCROLLBARS_INSIDE_OVERLAY or SCROLLBARS_INSIDE_INSET. If you want them to appear at the edge of the view, ignoring the padding, + ///then you can use SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET. + ///The default value is [AndroidScrollBarStyle.SCROLLBARS_INSIDE_OVERLAY]. + AndroidScrollBarStyle? scrollBarStyle; + + ///Sets the position of the vertical scroll bar. + ///The default value is [AndroidVerticalScrollbarPosition.SCROLLBAR_POSITION_DEFAULT]. + AndroidVerticalScrollbarPosition? verticalScrollbarPosition; + + ///Defines the delay in milliseconds that a scrollbar waits before fade out. + int? scrollBarDefaultDelayBeforeFade; + + ///Defines whether scrollbars will fade when the view is not scrolling. + ///The default value is `true`. + bool scrollbarFadingEnabled; + + ///Defines the scrollbar fade duration in milliseconds. + int? scrollBarFadeDuration; + + ///Sets the renderer priority policy for this WebView. + RendererPriorityPolicy? rendererPriorityPolicy; + + ///Sets whether the default Android error page should be disabled. + ///The default value is `false`. + bool? disableDefaultErrorPage; + + AndroidInAppWebViewOptions({ + this.textZoom = 100, + this.clearSessionCache = false, + this.builtInZoomControls = true, + this.displayZoomControls = false, + this.databaseEnabled = true, + this.domStorageEnabled = true, + this.useWideViewPort = true, + this.safeBrowsingEnabled = true, + this.mixedContentMode, + this.allowContentAccess = true, + this.allowFileAccess = true, + this.appCachePath, + this.blockNetworkImage = false, + this.blockNetworkLoads = false, + this.cacheMode = AndroidCacheMode.LOAD_DEFAULT, + this.cursiveFontFamily = "cursive", + this.defaultFixedFontSize = 16, + this.defaultFontSize = 16, + this.defaultTextEncodingName = "UTF-8", + this.disabledActionModeMenuItems, + this.fantasyFontFamily = "fantasy", + this.fixedFontFamily = "monospace", + this.forceDark = AndroidForceDark.FORCE_DARK_OFF, + this.geolocationEnabled = true, + this.layoutAlgorithm, + this.loadWithOverviewMode = true, + this.loadsImagesAutomatically = true, + this.minimumLogicalFontSize = 8, + this.needInitialFocus = true, + this.offscreenPreRaster = false, + this.sansSerifFontFamily = "sans-serif", + this.serifFontFamily = "sans-serif", + this.standardFontFamily = "sans-serif", + this.saveFormData = true, + this.thirdPartyCookiesEnabled = true, + this.hardwareAcceleration = true, + this.initialScale = 0, + this.supportMultipleWindows = false, + this.regexToCancelSubFramesLoading, + this.useHybridComposition = false, + this.useShouldInterceptRequest = false, + this.useOnRenderProcessGone = false, + this.overScrollMode = AndroidOverScrollMode.OVER_SCROLL_IF_CONTENT_SCROLLS, + this.networkAvailable, + this.scrollBarStyle = AndroidScrollBarStyle.SCROLLBARS_INSIDE_OVERLAY, + this.verticalScrollbarPosition = + AndroidVerticalScrollbarPosition.SCROLLBAR_POSITION_DEFAULT, + this.scrollBarDefaultDelayBeforeFade, + this.scrollbarFadingEnabled = true, + this.scrollBarFadeDuration, + this.rendererPriorityPolicy, + this.disableDefaultErrorPage, + }); + + @override + Map toMap() { + return { + "textZoom": textZoom, + "clearSessionCache": clearSessionCache, + "builtInZoomControls": builtInZoomControls, + "displayZoomControls": displayZoomControls, + "databaseEnabled": databaseEnabled, + "domStorageEnabled": domStorageEnabled, + "useWideViewPort": useWideViewPort, + "safeBrowsingEnabled": safeBrowsingEnabled, + "mixedContentMode": mixedContentMode?.toValue(), + "allowContentAccess": allowContentAccess, + "allowFileAccess": allowFileAccess, + "appCachePath": appCachePath, + "blockNetworkImage": blockNetworkImage, + "blockNetworkLoads": blockNetworkLoads, + "cacheMode": cacheMode?.toValue(), + "cursiveFontFamily": cursiveFontFamily, + "defaultFixedFontSize": defaultFixedFontSize, + "defaultFontSize": defaultFontSize, + "defaultTextEncodingName": defaultTextEncodingName, + "disabledActionModeMenuItems": disabledActionModeMenuItems?.toValue(), + "fantasyFontFamily": fantasyFontFamily, + "fixedFontFamily": fixedFontFamily, + "forceDark": forceDark?.toValue(), + "geolocationEnabled": geolocationEnabled, + "layoutAlgorithm": layoutAlgorithm?.toValue(), + "loadWithOverviewMode": loadWithOverviewMode, + "loadsImagesAutomatically": loadsImagesAutomatically, + "minimumLogicalFontSize": minimumLogicalFontSize, + "initialScale": initialScale, + "needInitialFocus": needInitialFocus, + "offscreenPreRaster": offscreenPreRaster, + "sansSerifFontFamily": sansSerifFontFamily, + "serifFontFamily": serifFontFamily, + "standardFontFamily": standardFontFamily, + "saveFormData": saveFormData, + "thirdPartyCookiesEnabled": thirdPartyCookiesEnabled, + "hardwareAcceleration": hardwareAcceleration, + "supportMultipleWindows": supportMultipleWindows, + "useHybridComposition": useHybridComposition, + "regexToCancelSubFramesLoading": regexToCancelSubFramesLoading, + "useShouldInterceptRequest": useShouldInterceptRequest, + "useOnRenderProcessGone": useOnRenderProcessGone, + "overScrollMode": overScrollMode?.toValue(), + "networkAvailable": networkAvailable, + "scrollBarStyle": scrollBarStyle?.toValue(), + "verticalScrollbarPosition": verticalScrollbarPosition?.toValue(), + "scrollBarDefaultDelayBeforeFade": scrollBarDefaultDelayBeforeFade, + "scrollbarFadingEnabled": scrollbarFadingEnabled, + "scrollBarFadeDuration": scrollBarFadeDuration, + "rendererPriorityPolicy": rendererPriorityPolicy?.toMap(), + "disableDefaultErrorPage": disableDefaultErrorPage + }; + } + + static AndroidInAppWebViewOptions fromMap(Map map) { + AndroidInAppWebViewOptions options = AndroidInAppWebViewOptions(); + options.textZoom = map["textZoom"]; + options.clearSessionCache = map["clearSessionCache"]; + options.builtInZoomControls = map["builtInZoomControls"]; + options.displayZoomControls = map["displayZoomControls"]; + options.databaseEnabled = map["databaseEnabled"]; + options.domStorageEnabled = map["domStorageEnabled"]; + options.useWideViewPort = map["useWideViewPort"]; + options.safeBrowsingEnabled = map["safeBrowsingEnabled"]; + options.mixedContentMode = + AndroidMixedContentMode.fromValue(map["mixedContentMode"]); + options.allowContentAccess = map["allowContentAccess"]; + options.allowFileAccess = map["allowFileAccess"]; + options.appCachePath = map["appCachePath"]; + options.blockNetworkImage = map["blockNetworkImage"]; + options.blockNetworkLoads = map["blockNetworkLoads"]; + options.cacheMode = AndroidCacheMode.fromValue(map["cacheMode"]); + options.cursiveFontFamily = map["cursiveFontFamily"]; + options.defaultFixedFontSize = map["defaultFixedFontSize"]; + options.defaultFontSize = map["defaultFontSize"]; + options.defaultTextEncodingName = map["defaultTextEncodingName"]; + options.disabledActionModeMenuItems = + AndroidActionModeMenuItem.fromValue(map["disabledActionModeMenuItems"]); + options.fantasyFontFamily = map["fantasyFontFamily"]; + options.fixedFontFamily = map["fixedFontFamily"]; + options.forceDark = AndroidForceDark.fromValue(map["forceDark"]); + options.geolocationEnabled = map["geolocationEnabled"]; + options.layoutAlgorithm = + AndroidLayoutAlgorithm.fromValue(map["layoutAlgorithm"]); + options.loadWithOverviewMode = map["loadWithOverviewMode"]; + options.loadsImagesAutomatically = map["loadsImagesAutomatically"]; + options.minimumLogicalFontSize = map["minimumLogicalFontSize"]; + options.initialScale = map["initialScale"]; + options.needInitialFocus = map["needInitialFocus"]; + options.offscreenPreRaster = map["offscreenPreRaster"]; + options.sansSerifFontFamily = map["sansSerifFontFamily"]; + options.serifFontFamily = map["serifFontFamily"]; + options.standardFontFamily = map["standardFontFamily"]; + options.saveFormData = map["saveFormData"]; + options.thirdPartyCookiesEnabled = map["thirdPartyCookiesEnabled"]; + options.hardwareAcceleration = map["hardwareAcceleration"]; + options.supportMultipleWindows = map["supportMultipleWindows"]; + options.regexToCancelSubFramesLoading = + map["regexToCancelSubFramesLoading"]; + options.useHybridComposition = map["useHybridComposition"]; + options.useShouldInterceptRequest = map["useShouldInterceptRequest"]; + options.useOnRenderProcessGone = map["useOnRenderProcessGone"]; + options.overScrollMode = + AndroidOverScrollMode.fromValue(map["overScrollMode"]); + options.networkAvailable = map["networkAvailable"]; + options.scrollBarStyle = + AndroidScrollBarStyle.fromValue(map["scrollBarStyle"]); + options.verticalScrollbarPosition = + AndroidVerticalScrollbarPosition.fromValue( + map["verticalScrollbarPosition"]); + options.scrollBarDefaultDelayBeforeFade = + map["scrollBarDefaultDelayBeforeFade"]; + options.scrollbarFadingEnabled = map["scrollbarFadingEnabled"]; + options.scrollBarFadeDuration = map["scrollBarFadeDuration"]; + options.rendererPriorityPolicy = RendererPriorityPolicy.fromMap( + map["rendererPriorityPolicy"]?.cast()); + options.disableDefaultErrorPage = map["disableDefaultErrorPage"]; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + AndroidInAppWebViewOptions copy() { + return AndroidInAppWebViewOptions.fromMap(this.toMap()); + } +} diff --git a/lib/src/in_app_webview/android/main.dart b/lib/src/in_app_webview/android/main.dart new file mode 100644 index 00000000..7725f881 --- /dev/null +++ b/lib/src/in_app_webview/android/main.dart @@ -0,0 +1,2 @@ +export 'in_app_webview_options.dart'; +export 'in_app_webview_controller.dart'; \ No newline at end of file diff --git a/lib/src/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart similarity index 84% rename from lib/src/headless_in_app_webview.dart rename to lib/src/in_app_webview/headless_in_app_webview.dart index cc9f89a0..9b46ac26 100644 --- a/lib/src/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -3,10 +3,12 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import 'context_menu.dart'; -import 'types.dart'; +import '../context_menu.dart'; +import '../types.dart'; +import '../_uuid_generator.dart'; import 'webview.dart'; import 'in_app_webview_controller.dart'; +import 'in_app_webview_options.dart'; ///Class that represents a WebView in headless mode. ///It can be used to run a WebView in background without attaching an `InAppWebView` to the widget tree. @@ -21,7 +23,7 @@ class HeadlessInAppWebView implements WebView { ///WebView Controller that can be used to access the [InAppWebViewController] API. late InAppWebViewController webViewController; - ///The window id of a [CreateWindowRequest.windowId]. + ///The window id of a [CreateWindowAction.windowId]. final int? windowId; HeadlessInAppWebView( @@ -78,14 +80,13 @@ class HeadlessInAppWebView implements WebView { this.iosOnDidReceiveServerRedirectForProvisionalNavigation, this.iosOnNavigationResponse, this.iosShouldAllowDeprecatedTLS, - this.initialUrl, + this.initialUrlRequest, this.initialFile, this.initialData, - this.initialHeaders, this.initialOptions, this.contextMenu, this.initialUserScripts}) { - uuid = uuidGenerator.v4(); + uuid = UUID_GENERATOR.v4(); webViewController = new InAppWebViewController(uuid, this); } @@ -112,10 +113,9 @@ class HeadlessInAppWebView implements WebView { args.putIfAbsent( 'params', () => { - 'initialUrl': this.initialUrl != null ? '${Uri.parse(this.initialUrl!)}' : '', + 'initialUrlRequest': (this.initialUrlRequest ?? URLRequest(url: Uri.parse("about:blank"))).toMap(), 'initialFile': this.initialFile, 'initialData': this.initialData?.toMap(), - 'initialHeaders': this.initialHeaders, 'initialOptions': this.initialOptions?.toMap() ?? {}, 'contextMenu': this.contextMenu?.toMap() ?? {}, 'windowId': this.windowId, @@ -152,7 +152,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function(InAppWebViewController controller, - String url, SafeBrowsingThreat? threatType)? androidOnSafeBrowsingHit; + Uri url, SafeBrowsingThreat? threatType)? androidOnSafeBrowsingHit; @override final InAppWebViewInitialData? initialData; @@ -160,9 +160,6 @@ class HeadlessInAppWebView implements WebView { @override final String? initialFile; - @override - final Map? initialHeaders; - @override final InAppWebViewGroupOptions? initialOptions; @@ -170,13 +167,13 @@ class HeadlessInAppWebView implements WebView { final ContextMenu? contextMenu; @override - final String? initialUrl; + final URLRequest? initialUrlRequest; @override final UnmodifiableListView? initialUserScripts; @override - final void Function(InAppWebViewController controller, String? url)? + final void Function(InAppWebViewController controller, Uri? url)? onPageCommitVisible; @override @@ -193,7 +190,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function(InAppWebViewController controller, - IOSNavigationResponse navigationResponse)? + IOSWKNavigationResponse navigationResponse)? iosOnNavigationResponse; @override @@ -218,7 +215,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function(InAppWebViewController controller, - CreateWindowRequest createWindowRequest)? onCreateWindow; + CreateWindowAction createWindowAction)? onCreateWindow; @override final void Function(InAppWebViewController controller)? onCloseWindow; @@ -230,7 +227,7 @@ class HeadlessInAppWebView implements WebView { final void Function(InAppWebViewController controller)? onWindowBlur; @override - final void Function(InAppWebViewController controller, String url)? + final void Function(InAppWebViewController controller, Uri url)? onDownloadStart; @override @@ -253,11 +250,11 @@ class HeadlessInAppWebView implements WebView { onJsPrompt; @override - final void Function(InAppWebViewController controller, String? url, int code, + final void Function(InAppWebViewController controller, Uri? url, int code, String message)? onLoadError; @override - final void Function(InAppWebViewController controller, String? url, + final void Function(InAppWebViewController controller, Uri? url, int statusCode, String description)? onLoadHttpError; @override @@ -267,22 +264,22 @@ class HeadlessInAppWebView implements WebView { @override final Future Function( - InAppWebViewController controller, String scheme, String url)? + InAppWebViewController controller, Uri url)? onLoadResourceCustomScheme; @override - final void Function(InAppWebViewController controller, String? url)? + final void Function(InAppWebViewController controller, Uri? url)? onLoadStart; @override - final void Function(InAppWebViewController controller, String? url)? onLoadStop; + final void Function(InAppWebViewController controller, Uri? url)? onLoadStop; @override final void Function(InAppWebViewController controller, InAppWebViewHitTestResult hitTestResult)? onLongPressHitTestResult; @override - final void Function(InAppWebViewController controller, String? url)? onPrint; + final void Function(InAppWebViewController controller, Uri? url)? onPrint; @override final void Function(InAppWebViewController controller, int progress)? @@ -290,7 +287,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function( - InAppWebViewController controller, ClientCertChallenge challenge)? + InAppWebViewController controller, URLAuthenticationChallenge challenge)? onReceivedClientCertRequest; @override @@ -300,7 +297,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function( - InAppWebViewController controller, ServerTrustChallenge challenge)? + InAppWebViewController controller, URLAuthenticationChallenge challenge)? onReceivedServerTrustAuthRequest; @override @@ -309,7 +306,7 @@ class HeadlessInAppWebView implements WebView { @override final void Function( - InAppWebViewController controller, String? url, bool? androidIsReload)? + InAppWebViewController controller, Uri? url, bool? androidIsReload)? onUpdateVisitedHistory; @override @@ -326,9 +323,8 @@ class HeadlessInAppWebView implements WebView { shouldInterceptFetchRequest; @override - final Future Function( - InAppWebViewController controller, - ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest)? + final Future Function( + InAppWebViewController controller, NavigationAction navigationAction)? shouldOverrideUrlLoading; @override @@ -344,12 +340,12 @@ class HeadlessInAppWebView implements WebView { @override final Future Function( - InAppWebViewController controller, String? url)? + InAppWebViewController controller, Uri? url)? androidOnRenderProcessUnresponsive; @override final Future Function( - InAppWebViewController controller, String? url)? + InAppWebViewController controller, Uri? url)? androidOnRenderProcessResponsive; @override @@ -359,7 +355,7 @@ class HeadlessInAppWebView implements WebView { @override final Future Function( - InAppWebViewController controller, String? url)? androidOnFormResubmission; + InAppWebViewController controller, Uri? url)? androidOnFormResubmission; @override final void Function( @@ -372,7 +368,7 @@ class HeadlessInAppWebView implements WebView { @override final void Function( - InAppWebViewController controller, String url, bool precomposed)? + InAppWebViewController controller, Uri url, bool precomposed)? androidOnReceivedTouchIconUrl; @override diff --git a/lib/src/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart similarity index 82% rename from lib/src/in_app_webview.dart rename to lib/src/in_app_webview/in_app_webview.dart index edd8c180..ad475e08 100755 --- a/lib/src/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -9,10 +9,12 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; -import 'context_menu.dart'; +import '../context_menu.dart'; +import '../types.dart'; + import 'webview.dart'; -import 'types.dart'; import 'in_app_webview_controller.dart'; +import 'in_app_webview_options.dart'; ///Flutter Widget for adding an **inline native WebView** integrated in the flutter widget tree. class InAppWebView extends StatefulWidget implements WebView { @@ -25,16 +27,15 @@ class InAppWebView extends StatefulWidget implements WebView { /// were not claimed by any other gesture recognizer. final Set>? gestureRecognizers; - ///The window id of a [CreateWindowRequest.windowId]. + ///The window id of a [CreateWindowAction.windowId]. final int? windowId; const InAppWebView({ Key? key, this.windowId, - this.initialUrl = "about:blank", + this.initialUrlRequest, this.initialFile, this.initialData, - this.initialHeaders = const {}, this.initialOptions, this.initialUserScripts, this.contextMenu, @@ -113,7 +114,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function(InAppWebViewController controller, - String url, SafeBrowsingThreat? threatType)? androidOnSafeBrowsingHit; + Uri url, SafeBrowsingThreat? threatType)? androidOnSafeBrowsingHit; @override final InAppWebViewInitialData? initialData; @@ -121,14 +122,11 @@ class InAppWebView extends StatefulWidget implements WebView { @override final String? initialFile; - @override - final Map? initialHeaders; - @override final InAppWebViewGroupOptions? initialOptions; @override - final String? initialUrl; + final URLRequest? initialUrlRequest; @override final UnmodifiableListView? initialUserScripts; @@ -137,7 +135,7 @@ class InAppWebView extends StatefulWidget implements WebView { final ContextMenu? contextMenu; @override - final void Function(InAppWebViewController controller, String? url)? + final void Function(InAppWebViewController controller, Uri? url)? onPageCommitVisible; @override @@ -154,7 +152,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function(InAppWebViewController controller, - IOSNavigationResponse navigationResponse)? + IOSWKNavigationResponse navigationResponse)? iosOnNavigationResponse; @override @@ -179,7 +177,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function(InAppWebViewController controller, - CreateWindowRequest createWindowRequest)? onCreateWindow; + CreateWindowAction createWindowAction)? onCreateWindow; @override final void Function(InAppWebViewController controller)? onCloseWindow; @@ -196,11 +194,11 @@ class InAppWebView extends StatefulWidget implements WebView { @override final void Function( - InAppWebViewController controller, String url, bool precomposed)? + InAppWebViewController controller, Uri url, bool precomposed)? androidOnReceivedTouchIconUrl; @override - final void Function(InAppWebViewController controller, String url)? + final void Function(InAppWebViewController controller, Uri url)? onDownloadStart; @override @@ -223,11 +221,11 @@ class InAppWebView extends StatefulWidget implements WebView { onJsPrompt; @override - final void Function(InAppWebViewController controller, String? url, int code, + final void Function(InAppWebViewController controller, Uri? url, int code, String message)? onLoadError; @override - final void Function(InAppWebViewController controller, String? url, + final void Function(InAppWebViewController controller, Uri? url, int statusCode, String description)? onLoadHttpError; @override @@ -237,22 +235,22 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function( - InAppWebViewController controller, String scheme, String url)? + InAppWebViewController controller, Uri url)? onLoadResourceCustomScheme; @override - final void Function(InAppWebViewController controller, String? url)? + final void Function(InAppWebViewController controller, Uri? url)? onLoadStart; @override - final void Function(InAppWebViewController controller, String? url)? onLoadStop; + final void Function(InAppWebViewController controller, Uri? url)? onLoadStop; @override final void Function(InAppWebViewController controller, InAppWebViewHitTestResult hitTestResult)? onLongPressHitTestResult; @override - final void Function(InAppWebViewController controller, String? url)? onPrint; + final void Function(InAppWebViewController controller, Uri? url)? onPrint; @override final void Function(InAppWebViewController controller, int progress)? @@ -260,7 +258,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function( - InAppWebViewController controller, ClientCertChallenge challenge)? + InAppWebViewController controller, URLAuthenticationChallenge challenge)? onReceivedClientCertRequest; @override @@ -270,7 +268,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function( - InAppWebViewController controller, ServerTrustChallenge challenge)? + InAppWebViewController controller, URLAuthenticationChallenge challenge)? onReceivedServerTrustAuthRequest; @override @@ -279,7 +277,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final void Function( - InAppWebViewController controller, String? url, bool? androidIsReload)? + InAppWebViewController controller, Uri? url, bool? androidIsReload)? onUpdateVisitedHistory; @override @@ -296,9 +294,8 @@ class InAppWebView extends StatefulWidget implements WebView { shouldInterceptFetchRequest; @override - final Future Function( - InAppWebViewController controller, - ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest)? + final Future Function( + InAppWebViewController controller, NavigationAction navigationAction)? shouldOverrideUrlLoading; @override @@ -314,12 +311,12 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function( - InAppWebViewController controller, String? url)? + InAppWebViewController controller, Uri? url)? androidOnRenderProcessUnresponsive; @override final Future Function( - InAppWebViewController controller, String? url)? + InAppWebViewController controller, Uri? url)? androidOnRenderProcessResponsive; @override @@ -329,7 +326,7 @@ class InAppWebView extends StatefulWidget implements WebView { @override final Future Function( - InAppWebViewController controller, String? url)? androidOnFormResubmission; + InAppWebViewController controller, Uri? url)? androidOnFormResubmission; @override final void Function( @@ -353,7 +350,7 @@ class _InAppWebViewState extends State { @override Widget build(BuildContext context) { if (defaultTargetPlatform == TargetPlatform.android) { - if (widget.initialOptions?.android?.useHybridComposition ?? false) { + if (widget.initialOptions?.android.useHybridComposition ?? false) { return PlatformViewLink( viewType: 'com.pichillilorenzo/flutter_inappwebview', surfaceFactory: ( @@ -372,10 +369,9 @@ class _InAppWebViewState extends State { viewType: 'com.pichillilorenzo/flutter_inappwebview', layoutDirection: TextDirection.rtl, creationParams: { - 'initialUrl': widget.initialUrl != null ? '${Uri.parse(widget.initialUrl!)}' : '', + 'initialUrlRequest': (widget.initialUrlRequest ?? URLRequest(url: Uri.parse("about:blank"))).toMap(), 'initialFile': widget.initialFile, 'initialData': widget.initialData?.toMap(), - 'initialHeaders': widget.initialHeaders, 'initialOptions': widget.initialOptions?.toMap() ?? {}, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, @@ -395,10 +391,9 @@ class _InAppWebViewState extends State { gestureRecognizers: widget.gestureRecognizers, layoutDirection: TextDirection.rtl, creationParams: { - 'initialUrl': widget.initialUrl != null ? '${Uri.parse(widget.initialUrl!)}' : '', + 'initialUrlRequest': (widget.initialUrlRequest ?? URLRequest(url: Uri.parse("about:blank"))).toMap(), 'initialFile': widget.initialFile, 'initialData': widget.initialData?.toMap(), - 'initialHeaders': widget.initialHeaders, 'initialOptions': widget.initialOptions?.toMap() ?? {}, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, @@ -413,10 +408,9 @@ class _InAppWebViewState extends State { onPlatformViewCreated: _onPlatformViewCreated, gestureRecognizers: widget.gestureRecognizers, creationParams: { - 'initialUrl': widget.initialUrl != null ? '${Uri.parse(widget.initialUrl!)}' : '', + 'initialUrlRequest': (widget.initialUrlRequest ?? URLRequest(url: Uri.parse("about:blank"))).toMap(), 'initialFile': widget.initialFile, 'initialData': widget.initialData?.toMap(), - 'initialHeaders': widget.initialHeaders, 'initialOptions': widget.initialOptions?.toMap() ?? {}, 'contextMenu': widget.contextMenu?.toMap() ?? {}, 'windowId': widget.windowId, diff --git a/lib/src/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart similarity index 51% rename from lib/src/in_app_webview_controller.dart rename to lib/src/in_app_webview/in_app_webview_controller.dart index 8a15b7be..7548039a 100644 --- a/lib/src/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -9,21 +9,24 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'X509Certificate/asn1_distinguished_names.dart'; -import 'X509Certificate/x509_certificate.dart'; -import 'context_menu.dart'; -import 'types.dart'; -import 'in_app_browser.dart'; -import 'webview_options.dart'; +import 'android/in_app_webview_controller.dart'; +import 'ios/in_app_webview_controller.dart'; + +import '../context_menu.dart'; +import '../types.dart'; +import '../in_app_browser/in_app_browser.dart'; +import '../web_storage/web_storage.dart'; +import '../util.dart'; + import 'headless_in_app_webview.dart'; -import 'webview.dart'; import 'in_app_webview.dart'; -import 'web_storage.dart'; -import 'util.dart'; +import 'in_app_webview_options.dart'; +import 'webview.dart'; +import '_static_channel.dart'; ///List of forbidden names for JavaScript handlers. -const javaScriptHandlerForbiddenNames = [ +final _JAVASCRIPT_HANDLER_FORBIDDEN_NAMES = UnmodifiableListView([ "onLoadResource", "shouldInterceptAjaxRequest", "onAjaxReadyStateChange", @@ -32,8 +35,9 @@ const javaScriptHandlerForbiddenNames = [ "onPrint", "onWindowFocus", "onWindowBlur", - "callAsyncJavaScript" -]; + "callAsyncJavaScript", + "evaluateJavaScriptWithContentWorld" +]); ///Controls a WebView, such as an [InAppWebView] widget instance, a [HeadlessInAppWebView] instance or [InAppBrowser] WebView instance. /// @@ -42,8 +46,7 @@ const javaScriptHandlerForbiddenNames = [ class InAppWebViewController { WebView? _webview; late MethodChannel _channel; - static MethodChannel _staticChannel = - MethodChannel('com.pichillilorenzo/flutter_inappwebview_static'); + static MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; Map javaScriptHandlersMap = HashMap(); List _userScripts = []; @@ -88,8 +91,8 @@ class InAppWebViewController { } void _init() { - this.android = AndroidInAppWebViewController(this); - this.ios = IOSInAppWebViewController(this); + this.android = AndroidInAppWebViewController(channel: _channel); + this.ios = IOSInAppWebViewController(channel: _channel); this.webStorage = WebStorage( localStorage: LocalStorage(this), sessionStorage: SessionStorage(this)); } @@ -101,525 +104,398 @@ class InAppWebViewController { _webview!.onWebViewCreated!(this); break; case "onLoadStart": - String? url = call.arguments["url"]; - if (_webview != null && _webview!.onLoadStart != null) - _webview!.onLoadStart!(this, url); - else if (_inAppBrowser != null) _inAppBrowser!.onLoadStart(url); + if ((_webview != null && _webview!.onLoadStart != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onLoadStart != null) + _webview!.onLoadStart!(this, uri); + else + _inAppBrowser!.onLoadStart(uri); + } break; case "onLoadStop": - String? url = call.arguments["url"]; - if (_webview != null && _webview!.onLoadStop != null) - _webview!.onLoadStop!(this, url); - else if (_inAppBrowser != null) _inAppBrowser!.onLoadStop(url); + if ((_webview != null && _webview!.onLoadStop != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onLoadStop != null) + _webview!.onLoadStop!(this, uri); + else + _inAppBrowser!.onLoadStop(uri); + } break; case "onLoadError": - String? url = call.arguments["url"]; - int code = call.arguments["code"]; - String message = call.arguments["message"]; - if (_webview != null && _webview!.onLoadError != null) - _webview!.onLoadError!(this, url, code, message); - else if (_inAppBrowser != null) - _inAppBrowser!.onLoadError(url, code, message); + if ((_webview != null && _webview!.onLoadError != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + int code = call.arguments["code"]; + String message = call.arguments["message"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onLoadError != null) + _webview!.onLoadError!(this, uri, code, message); + else + _inAppBrowser!.onLoadError(uri, code, message); + } break; case "onLoadHttpError": - String? url = call.arguments["url"]; - int statusCode = call.arguments["statusCode"]; - String description = call.arguments["description"]; - if (_webview != null && _webview!.onLoadHttpError != null) - _webview!.onLoadHttpError!(this, url, statusCode, description); - else if (_inAppBrowser != null) - _inAppBrowser!.onLoadHttpError(url, statusCode, description); + if ((_webview != null && _webview!.onLoadHttpError != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + int statusCode = call.arguments["statusCode"]; + String description = call.arguments["description"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onLoadHttpError != null) + _webview!.onLoadHttpError!(this, uri, statusCode, description); + else + _inAppBrowser!.onLoadHttpError(uri, statusCode, description); + } break; case "onProgressChanged": - int progress = call.arguments["progress"]; - if (_webview != null && _webview!.onProgressChanged != null) - _webview!.onProgressChanged!(this, progress); - else if (_inAppBrowser != null) - _inAppBrowser!.onProgressChanged(progress); + if ((_webview != null && _webview!.onProgressChanged != null) || _inAppBrowser != null) { + int progress = call.arguments["progress"]; + if (_webview != null && _webview!.onProgressChanged != null) + _webview!.onProgressChanged!(this, progress); + else + _inAppBrowser!.onProgressChanged(progress); + } break; case "shouldOverrideUrlLoading": - String url = call.arguments["url"]; - String? method = call.arguments["method"]; - Map? headers = - call.arguments["headers"]?.cast(); - bool isForMainFrame = call.arguments["isForMainFrame"]; - bool? androidHasGesture = call.arguments["androidHasGesture"]; - bool? androidIsRedirect = call.arguments["androidIsRedirect"]; - int? iosWKNavigationType = call.arguments["iosWKNavigationType"]; - bool? iosAllowsCellularAccess = call.arguments["iosAllowsCellularAccess"]; - bool? iosAllowsConstrainedNetworkAccess = call.arguments["iosAllowsConstrainedNetworkAccess"]; - bool? iosAllowsExpensiveNetworkAccess = call.arguments["iosAllowsExpensiveNetworkAccess"]; - int? iosCachePolicy = call.arguments["iosCachePolicy"]; - bool? iosHttpShouldHandleCookies = call.arguments["iosHttpShouldHandleCookies"]; - bool? iosHttpShouldUsePipelining = call.arguments["iosHttpShouldUsePipelining"]; - int? iosNetworkServiceType = call.arguments["iosNetworkServiceType"]; - double? iosTimeoutInterval = call.arguments["iosTimeoutInterval"]; + if ((_webview != null && _webview!.shouldOverrideUrlLoading != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + NavigationAction navigationAction = NavigationAction.fromMap(arguments)!; - ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest = - ShouldOverrideUrlLoadingRequest( - url: url, - method: method, - headers: headers, - isForMainFrame: isForMainFrame, - androidHasGesture: androidHasGesture, - androidIsRedirect: androidIsRedirect, - iosWKNavigationType: - IOSWKNavigationType.fromValue(iosWKNavigationType), - iosAllowsCellularAccess: iosAllowsCellularAccess, - iosAllowsConstrainedNetworkAccess: iosAllowsConstrainedNetworkAccess, - iosAllowsExpensiveNetworkAccess: iosAllowsExpensiveNetworkAccess, - iosCachePolicy: IOSURLRequestCachePolicy.fromValue(iosCachePolicy), - iosHttpShouldHandleCookies: iosHttpShouldHandleCookies, - iosHttpShouldUsePipelining: iosHttpShouldUsePipelining, - iosNetworkServiceType: IOSURLRequestNetworkServiceType.fromValue(iosNetworkServiceType), - iosTimeoutInterval: iosTimeoutInterval); - - if (_webview != null && _webview!.shouldOverrideUrlLoading != null) - return (await _webview!.shouldOverrideUrlLoading!( - this, shouldOverrideUrlLoadingRequest)) - ?.toMap(); - else if (_inAppBrowser != null) + if (_webview != null && _webview!.shouldOverrideUrlLoading != null) + return (await _webview!.shouldOverrideUrlLoading!( + this, navigationAction)) + ?.toMap(); return (await _inAppBrowser! - .shouldOverrideUrlLoading(shouldOverrideUrlLoadingRequest)) + .shouldOverrideUrlLoading(navigationAction)) ?.toMap(); + } break; case "onConsoleMessage": - String message = call.arguments["message"]; - ConsoleMessageLevel? messageLevel = - ConsoleMessageLevel.fromValue(call.arguments["messageLevel"]); - ConsoleMessage consoleMessage = - ConsoleMessage(message: message, messageLevel: messageLevel); - if (_webview != null && _webview!.onConsoleMessage != null) - _webview!.onConsoleMessage!(this, consoleMessage); - else if (_inAppBrowser != null) - _inAppBrowser!.onConsoleMessage(consoleMessage); + if ((_webview != null && _webview!.onConsoleMessage != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + ConsoleMessage consoleMessage = ConsoleMessage.fromMap(arguments)!; + if (_webview != null && _webview!.onConsoleMessage != null) + _webview!.onConsoleMessage!(this, consoleMessage); + else + _inAppBrowser!.onConsoleMessage(consoleMessage); + } break; case "onScrollChanged": - int x = call.arguments["x"]; - int y = call.arguments["y"]; - if (_webview != null && _webview!.onScrollChanged != null) - _webview!.onScrollChanged!(this, x, y); - else if (_inAppBrowser != null) _inAppBrowser!.onScrollChanged(x, y); + if ((_webview != null && _webview!.onScrollChanged != null) || _inAppBrowser != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + if (_webview != null && _webview!.onScrollChanged != null) + _webview!.onScrollChanged!(this, x, y); + else + _inAppBrowser!.onScrollChanged(x, y); + } break; case "onDownloadStart": - String url = call.arguments["url"]; - if (_webview != null && _webview!.onDownloadStart != null) - _webview!.onDownloadStart!(this, url); - else if (_inAppBrowser != null) _inAppBrowser!.onDownloadStart(url); + if ((_webview != null && _webview!.onDownloadStart != null) || _inAppBrowser != null) { + String url = call.arguments["url"]; + Uri uri = Uri.parse(url); + if (_webview != null && _webview!.onDownloadStart != null) + _webview!.onDownloadStart!(this, uri); + else + _inAppBrowser!.onDownloadStart(uri); + } break; case "onLoadResourceCustomScheme": - String scheme = call.arguments["scheme"]; - String url = call.arguments["url"]; - if (_webview != null && _webview!.onLoadResourceCustomScheme != null) { - try { - var response = - await _webview!.onLoadResourceCustomScheme!(this, scheme, url); - return (response != null) ? response.toJson() : null; - } catch (error) { - print(error); - return null; - } - } else if (_inAppBrowser != null) { - try { - var response = - await _inAppBrowser!.onLoadResourceCustomScheme(scheme, url); - return (response != null) ? response.toJson() : null; - } catch (error) { - print(error); - return null; - } + if ((_webview != null && _webview!.onLoadResourceCustomScheme != null) || _inAppBrowser != null) { + String url = call.arguments["url"]; + Uri uri = Uri.parse(url); + if (_webview != null && _webview!.onLoadResourceCustomScheme != null) + return (await _webview!.onLoadResourceCustomScheme!(this, uri))?.toMap(); + else + return (await _inAppBrowser!.onLoadResourceCustomScheme(uri))?.toMap(); } break; case "onCreateWindow": - String? url = call.arguments["url"]; - int windowId = call.arguments["windowId"]; - bool? androidIsDialog = call.arguments["androidIsDialog"]; - bool? androidIsUserGesture = call.arguments["androidIsUserGesture"]; - int? iosWKNavigationType = call.arguments["iosWKNavigationType"]; - bool? iosIsForMainFrame = call.arguments["iosIsForMainFrame"]; - bool? iosAllowsCellularAccess = call.arguments["iosAllowsCellularAccess"]; - bool? iosAllowsConstrainedNetworkAccess = call.arguments["iosAllowsConstrainedNetworkAccess"]; - bool? iosAllowsExpensiveNetworkAccess = call.arguments["iosAllowsExpensiveNetworkAccess"]; - int? iosCachePolicy = call.arguments["iosCachePolicy"]; - bool? iosHttpShouldHandleCookies = call.arguments["iosHttpShouldHandleCookies"]; - bool? iosHttpShouldUsePipelining = call.arguments["iosHttpShouldUsePipelining"]; - int? iosNetworkServiceType = call.arguments["iosNetworkServiceType"]; - double? iosTimeoutInterval = call.arguments["iosTimeoutInterval"]; + if ((_webview != null && _webview!.onCreateWindow != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + CreateWindowAction createWindowAction = CreateWindowAction.fromMap(arguments)!; - CreateWindowRequest createWindowRequest = CreateWindowRequest( - url: url, - windowId: windowId, - androidIsDialog: androidIsDialog, - androidIsUserGesture: androidIsUserGesture, - iosWKNavigationType: - IOSWKNavigationType.fromValue(iosWKNavigationType), - iosIsForMainFrame: iosIsForMainFrame, - iosAllowsCellularAccess: iosAllowsCellularAccess, - iosAllowsConstrainedNetworkAccess: iosAllowsConstrainedNetworkAccess, - iosAllowsExpensiveNetworkAccess: iosAllowsExpensiveNetworkAccess, - iosCachePolicy: IOSURLRequestCachePolicy.fromValue(iosCachePolicy), - iosHttpShouldHandleCookies: iosHttpShouldHandleCookies, - iosHttpShouldUsePipelining: iosHttpShouldUsePipelining, - iosNetworkServiceType: IOSURLRequestNetworkServiceType.fromValue(iosNetworkServiceType), - iosTimeoutInterval: iosTimeoutInterval); - - bool? result = false; - - if (_webview != null && _webview!.onCreateWindow != null) - result = await _webview!.onCreateWindow!(this, createWindowRequest); - else if (_inAppBrowser != null) { - result = await _inAppBrowser!.onCreateWindow(createWindowRequest); + if (_webview != null && _webview!.onCreateWindow != null) + return await _webview!.onCreateWindow!(this, createWindowAction); + else + return await _inAppBrowser!.onCreateWindow(createWindowAction); } - - return result; + break; case "onCloseWindow": if (_webview != null && _webview!.onCloseWindow != null) _webview!.onCloseWindow!(this); else if (_inAppBrowser != null) _inAppBrowser!.onCloseWindow(); break; case "onTitleChanged": - String? title = call.arguments["title"]; - if (_webview != null && _webview!.onTitleChanged != null) - _webview!.onTitleChanged!(this, title); - else if (_inAppBrowser != null) _inAppBrowser!.onTitleChanged(title); + if ((_webview != null && _webview!.onTitleChanged != null) || _inAppBrowser != null) { + String? title = call.arguments["title"]; + if (_webview != null && _webview!.onTitleChanged != null) + _webview!.onTitleChanged!(this, title); + else + _inAppBrowser!.onTitleChanged(title); + } break; case "onGeolocationPermissionsShowPrompt": - String origin = call.arguments["origin"]; - if (_webview != null && - _webview!.androidOnGeolocationPermissionsShowPrompt != null) - return (await _webview!.androidOnGeolocationPermissionsShowPrompt!( - this, origin)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser! - .androidOnGeolocationPermissionsShowPrompt(origin)) - ?.toMap(); + if ((_webview != null && _webview!.androidOnGeolocationPermissionsShowPrompt != null) || _inAppBrowser != null) { + String origin = call.arguments["origin"]; + if (_webview != null && _webview!.androidOnGeolocationPermissionsShowPrompt != null) + return (await _webview!.androidOnGeolocationPermissionsShowPrompt!( + this, origin)) + ?.toMap(); + else + return (await _inAppBrowser! + .androidOnGeolocationPermissionsShowPrompt(origin)) + ?.toMap(); + } break; case "onGeolocationPermissionsHidePrompt": - if (_webview != null && - _webview!.androidOnGeolocationPermissionsHidePrompt != null) + if (_webview != null && _webview!.androidOnGeolocationPermissionsHidePrompt != null) _webview!.androidOnGeolocationPermissionsHidePrompt!(this); else if (_inAppBrowser != null) _inAppBrowser!.androidOnGeolocationPermissionsHidePrompt(); break; case "shouldInterceptRequest": - String url = call.arguments["url"]; - String method = call.arguments["method"]; - Map? headers = - call.arguments["headers"]?.cast(); - bool isForMainFrame = call.arguments["isForMainFrame"]; - bool hasGesture = call.arguments["hasGesture"]; - bool isRedirect = call.arguments["isRedirect"]; + if ((_webview != null && _webview!.androidShouldInterceptRequest != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; - var request = new WebResourceRequest( - url: url, - method: method, - headers: headers, - isForMainFrame: isForMainFrame, - hasGesture: hasGesture, - isRedirect: isRedirect); - - if (_webview != null && _webview!.androidShouldInterceptRequest != null) - return (await _webview!.androidShouldInterceptRequest!(this, request)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidShouldInterceptRequest(request)) - ?.toMap(); + if (_webview != null && _webview!.androidShouldInterceptRequest != null) + return (await _webview!.androidShouldInterceptRequest!(this, request)) + ?.toMap(); + else + return (await _inAppBrowser!.androidShouldInterceptRequest(request)) + ?.toMap(); + } break; case "onRenderProcessUnresponsive": - String? url = call.arguments["url"]; - if (_webview != null && - _webview!.androidOnRenderProcessUnresponsive != null) - return (await _webview!.androidOnRenderProcessUnresponsive!(this, url)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidOnRenderProcessUnresponsive(url)) - ?.toMap(); - break; + if ((_webview != null && _webview!.androidOnRenderProcessUnresponsive != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.androidOnRenderProcessUnresponsive != null) + return (await _webview!.androidOnRenderProcessUnresponsive!(this, uri)) + ?.toMap(); + else + return (await _inAppBrowser!.androidOnRenderProcessUnresponsive(uri)) + ?.toMap(); + } + break; case "onRenderProcessResponsive": - String? url = call.arguments["url"]; - if (_webview != null && - _webview!.androidOnRenderProcessResponsive != null) - return (await _webview!.androidOnRenderProcessResponsive!(this, url)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidOnRenderProcessResponsive(url)) - ?.toMap(); + if ((_webview != null && _webview!.androidOnRenderProcessResponsive != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.androidOnRenderProcessResponsive != null) + return (await _webview!.androidOnRenderProcessResponsive!(this, uri)) + ?.toMap(); + else + return (await _inAppBrowser!.androidOnRenderProcessResponsive(uri)) + ?.toMap(); + } break; case "onRenderProcessGone": - bool didCrash = call.arguments["didCrash"]; - RendererPriority? rendererPriorityAtExit = RendererPriority.fromValue( - call.arguments["rendererPriorityAtExit"]); - var detail = RenderProcessGoneDetail( - didCrash: didCrash, rendererPriorityAtExit: rendererPriorityAtExit); + if ((_webview != null && _webview!.androidOnRenderProcessGone != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + RenderProcessGoneDetail detail = RenderProcessGoneDetail.fromMap(arguments)!; - if (_webview != null && _webview!.androidOnRenderProcessGone != null) - _webview!.androidOnRenderProcessGone!(this, detail); - else if (_inAppBrowser != null) - _inAppBrowser!.androidOnRenderProcessGone(detail); + if (_webview != null && _webview!.androidOnRenderProcessGone != null) + _webview!.androidOnRenderProcessGone!(this, detail); + else + _inAppBrowser!.androidOnRenderProcessGone(detail); + } break; case "onFormResubmission": - String? url = call.arguments["url"]; - if (_webview != null && _webview!.androidOnFormResubmission != null) - return (await _webview!.androidOnFormResubmission!(this, url))?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidOnFormResubmission(url))?.toMap(); + if ((_webview != null && _webview!.androidOnFormResubmission != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.androidOnFormResubmission != null) + return (await _webview!.androidOnFormResubmission!(this, uri))?.toMap(); + else + return (await _inAppBrowser!.androidOnFormResubmission(uri))?.toMap(); + } break; case "onScaleChanged": - double oldScale = call.arguments["oldScale"]; - double newScale = call.arguments["newScale"]; - if (_webview != null && _webview!.androidOnScaleChanged != null) - _webview!.androidOnScaleChanged!(this, oldScale, newScale); - else if (_inAppBrowser != null) - _inAppBrowser!.androidOnScaleChanged(oldScale, newScale); + if ((_webview != null && _webview!.androidOnScaleChanged != null) || _inAppBrowser != null) { + double oldScale = call.arguments["oldScale"]; + double newScale = call.arguments["newScale"]; + if (_webview != null && _webview!.androidOnScaleChanged != null) + _webview!.androidOnScaleChanged!(this, oldScale, newScale); + else + _inAppBrowser!.androidOnScaleChanged(oldScale, newScale); + } break; case "onReceivedIcon": - Uint8List icon = Uint8List.fromList(call.arguments["icon"].cast()); + if ((_webview != null && _webview!.androidOnReceivedIcon != null) || _inAppBrowser != null) { + Uint8List icon = Uint8List.fromList(call.arguments["icon"].cast()); - if (_webview != null && _webview!.androidOnReceivedIcon != null) - _webview!.androidOnReceivedIcon!(this, icon); - else if (_inAppBrowser != null) - _inAppBrowser!.androidOnReceivedIcon(icon); + if (_webview != null && _webview!.androidOnReceivedIcon != null) + _webview!.androidOnReceivedIcon!(this, icon); + else + _inAppBrowser!.androidOnReceivedIcon(icon); + } break; case "onReceivedTouchIconUrl": - String url = call.arguments["url"]; - bool precomposed = call.arguments["precomposed"]; - if (_webview != null && _webview!.androidOnReceivedTouchIconUrl != null) - _webview!.androidOnReceivedTouchIconUrl!(this, url, precomposed); - else if (_inAppBrowser != null) - _inAppBrowser!.androidOnReceivedTouchIconUrl(url, precomposed); + if ((_webview != null && _webview!.androidOnReceivedTouchIconUrl != null) || _inAppBrowser != null) { + String url = call.arguments["url"]; + bool precomposed = call.arguments["precomposed"]; + Uri uri = Uri.parse(url); + if (_webview != null && _webview!.androidOnReceivedTouchIconUrl != null) + _webview!.androidOnReceivedTouchIconUrl!(this, uri, precomposed); + else + _inAppBrowser!.androidOnReceivedTouchIconUrl(uri, precomposed); + } break; case "onJsAlert": - String? url = call.arguments["url"]; - String? message = call.arguments["message"]; - bool? iosIsMainFrame = call.arguments["iosIsMainFrame"]; + if ((_webview != null && _webview!.onJsAlert != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + JsAlertRequest jsAlertRequest = JsAlertRequest.fromMap(arguments)!; - JsAlertRequest jsAlertRequest = JsAlertRequest( - url: url, message: message, iosIsMainFrame: iosIsMainFrame); - - if (_webview != null && _webview!.onJsAlert != null) - return (await _webview!.onJsAlert!(this, jsAlertRequest))?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.onJsAlert(jsAlertRequest))?.toMap(); + if (_webview != null && _webview!.onJsAlert != null) + return (await _webview!.onJsAlert!(this, jsAlertRequest))?.toMap(); + else + return (await _inAppBrowser!.onJsAlert(jsAlertRequest))?.toMap(); + } break; case "onJsConfirm": - String? url = call.arguments["url"]; - String? message = call.arguments["message"]; - bool? iosIsMainFrame = call.arguments["iosIsMainFrame"]; + if ((_webview != null && _webview!.onJsConfirm != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + JsConfirmRequest jsConfirmRequest = JsConfirmRequest.fromMap(arguments)!; - JsConfirmRequest jsConfirmRequest = JsConfirmRequest( - url: url, message: message, iosIsMainFrame: iosIsMainFrame); - - if (_webview != null && _webview!.onJsConfirm != null) - return (await _webview!.onJsConfirm!(this, jsConfirmRequest))?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.onJsConfirm(jsConfirmRequest))?.toMap(); + if (_webview != null && _webview!.onJsConfirm != null) + return (await _webview!.onJsConfirm!(this, jsConfirmRequest))?.toMap(); + else + return (await _inAppBrowser!.onJsConfirm(jsConfirmRequest))?.toMap(); + } break; case "onJsPrompt": - String? url = call.arguments["url"]; - String? message = call.arguments["message"]; - String? defaultValue = call.arguments["defaultValue"]; - bool? iosIsMainFrame = call.arguments["iosIsMainFrame"]; + if ((_webview != null && _webview!.onJsPrompt != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + JsPromptRequest jsPromptRequest = JsPromptRequest.fromMap(arguments)!; - JsPromptRequest jsPromptRequest = JsPromptRequest( - url: url, - message: message, - defaultValue: defaultValue, - iosIsMainFrame: iosIsMainFrame); - - if (_webview != null && _webview!.onJsPrompt != null) - return (await _webview!.onJsPrompt!(this, jsPromptRequest))?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.onJsPrompt(jsPromptRequest))?.toMap(); + if (_webview != null && _webview!.onJsPrompt != null) + return (await _webview!.onJsPrompt!(this, jsPromptRequest))?.toMap(); + else + return (await _inAppBrowser!.onJsPrompt(jsPromptRequest))?.toMap(); + } break; case "onJsBeforeUnload": - String? url = call.arguments["url"]; - String? message = call.arguments["message"]; - bool? iosIsMainFrame = call.arguments["iosIsMainFrame"]; + if ((_webview != null && _webview!.androidOnJsBeforeUnload != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + JsBeforeUnloadRequest jsBeforeUnloadRequest = JsBeforeUnloadRequest.fromMap(arguments)!; - JsBeforeUnloadRequest jsBeforeUnloadRequest = JsBeforeUnloadRequest( - url: url, message: message, iosIsMainFrame: iosIsMainFrame); - - print(jsBeforeUnloadRequest); - - if (_webview != null && _webview!.androidOnJsBeforeUnload != null) - return (await _webview!.androidOnJsBeforeUnload!( - this, jsBeforeUnloadRequest)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser! - .androidOnJsBeforeUnload(jsBeforeUnloadRequest)) - ?.toMap(); + if (_webview != null && _webview!.androidOnJsBeforeUnload != null) + return (await _webview!.androidOnJsBeforeUnload!( + this, jsBeforeUnloadRequest)) + ?.toMap(); + else + return (await _inAppBrowser! + .androidOnJsBeforeUnload(jsBeforeUnloadRequest)) + ?.toMap(); + } break; case "onSafeBrowsingHit": - String url = call.arguments["url"]; - SafeBrowsingThreat? threatType = - SafeBrowsingThreat.fromValue(call.arguments["threatType"]); - if (_webview != null && _webview!.androidOnSafeBrowsingHit != null) - return (await _webview!.androidOnSafeBrowsingHit!( - this, url, threatType)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidOnSafeBrowsingHit(url, threatType)) - ?.toMap(); + if ((_webview != null && _webview!.androidOnSafeBrowsingHit != null) || _inAppBrowser != null) { + String url = call.arguments["url"]; + SafeBrowsingThreat? threatType = + SafeBrowsingThreat.fromValue(call.arguments["threatType"]); + Uri uri = Uri.parse(url); + if (_webview != null && _webview!.androidOnSafeBrowsingHit != null) + return (await _webview!.androidOnSafeBrowsingHit!( + this, uri, threatType)) + ?.toMap(); + else + return (await _inAppBrowser!.androidOnSafeBrowsingHit(uri, threatType)) + ?.toMap(); + } break; case "onReceivedLoginRequest": - String realm = call.arguments["realm"]; - String? account = call.arguments["account"]; - String args = call.arguments["args"]; + if ((_webview != null && _webview!.androidOnReceivedLoginRequest != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + LoginRequest loginRequest = LoginRequest.fromMap(arguments)!; - LoginRequest loginRequest = - LoginRequest(realm: realm, account: account, args: args); - - if (_webview != null && _webview!.androidOnReceivedLoginRequest != null) - _webview!.androidOnReceivedLoginRequest!(this, loginRequest); - else if (_inAppBrowser != null) - _inAppBrowser!.androidOnReceivedLoginRequest(loginRequest); + if (_webview != null && _webview!.androidOnReceivedLoginRequest != null) + _webview!.androidOnReceivedLoginRequest!(this, loginRequest); + else + _inAppBrowser!.androidOnReceivedLoginRequest(loginRequest); + } break; case "onReceivedHttpAuthRequest": - String host = call.arguments["host"]; - String protocol = call.arguments["protocol"]; - String? realm = call.arguments["realm"]; - int? port = call.arguments["port"]; - int previousFailureCount = call.arguments["previousFailureCount"]; - var protectionSpace = ProtectionSpace( - host: host, protocol: protocol, realm: realm, port: port); - var challenge = URLAuthenticationChallenge( - previousFailureCount: previousFailureCount, - protectionSpace: protectionSpace); - if (_webview != null && _webview!.onReceivedHttpAuthRequest != null) - return (await _webview!.onReceivedHttpAuthRequest!(this, challenge)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.onReceivedHttpAuthRequest(challenge)) - ?.toMap(); + if ((_webview != null && _webview!.onReceivedHttpAuthRequest != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + HttpAuthenticationChallenge challenge = HttpAuthenticationChallenge.fromMap(arguments)!; + + if (_webview != null && _webview!.onReceivedHttpAuthRequest != null) + return (await _webview!.onReceivedHttpAuthRequest!(this, challenge)) + ?.toMap(); + else + return (await _inAppBrowser!.onReceivedHttpAuthRequest(challenge)) + ?.toMap(); + } break; case "onReceivedServerTrustAuthRequest": - String host = call.arguments["host"]; - String protocol = call.arguments["protocol"]; - String? realm = call.arguments["realm"]; - int? port = call.arguments["port"]; - int? androidError = call.arguments["androidError"]; - int? iosError = call.arguments["iosError"]; - String? message = call.arguments["message"]; - Map? sslCertificateMap = - call.arguments["sslCertificate"]?.cast(); + if ((_webview != null && _webview!.onReceivedServerTrustAuthRequest != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + ServerTrustChallenge challenge = ServerTrustChallenge.fromMap(arguments)!; - SslCertificate? sslCertificate; - if (sslCertificateMap != null) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - try { - X509Certificate x509certificate = X509Certificate.fromData( - data: sslCertificateMap["x509Certificate"]); - sslCertificate = SslCertificate( - issuedBy: SslCertificateDName( - CName: x509certificate.issuer( - dn: ASN1DistinguishedNames.COMMON_NAME) ?? - "", - DName: x509certificate.issuerDistinguishedName ?? "", - OName: x509certificate.issuer( - dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? - "", - UName: x509certificate.issuer( - dn: ASN1DistinguishedNames - .ORGANIZATIONAL_UNIT_NAME) ?? - ""), - issuedTo: SslCertificateDName( - CName: x509certificate.subject( - dn: ASN1DistinguishedNames.COMMON_NAME) ?? - "", - DName: x509certificate.subjectDistinguishedName ?? "", - OName: x509certificate.subject( - dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? - "", - UName: x509certificate.subject( - dn: ASN1DistinguishedNames - .ORGANIZATIONAL_UNIT_NAME) ?? - ""), - validNotAfterDate: x509certificate.notAfter, - validNotBeforeDate: x509certificate.notBefore, - x509Certificate: x509certificate, - ); - } catch (e, stacktrace) { - print(e); - print(stacktrace); - return null; - } - } else { - sslCertificate = SslCertificate.fromMap(sslCertificateMap); - } + if (_webview != null && _webview!.onReceivedServerTrustAuthRequest != null) + return (await _webview!.onReceivedServerTrustAuthRequest!( + this, challenge)) + ?.toMap(); + else + return (await _inAppBrowser! + .onReceivedServerTrustAuthRequest(challenge)) + ?.toMap(); } - - AndroidSslError? androidSslError = androidError != null - ? AndroidSslError.fromValue(androidError) - : null; - IOSSslError? iosSslError = - iosError != null ? IOSSslError.fromValue(iosError) : null; - - var protectionSpace = ProtectionSpace( - host: host, protocol: protocol, realm: realm, port: port); - var challenge = ServerTrustChallenge( - protectionSpace: protectionSpace, - androidError: androidSslError, - iosError: iosSslError, - message: message, - sslCertificate: sslCertificate); - if (_webview != null && - _webview!.onReceivedServerTrustAuthRequest != null) - return (await _webview!.onReceivedServerTrustAuthRequest!( - this, challenge)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser! - .onReceivedServerTrustAuthRequest(challenge)) - ?.toMap(); break; case "onReceivedClientCertRequest": - String host = call.arguments["host"]; - String protocol = call.arguments["protocol"]; - String? realm = call.arguments["realm"]; - int? port = call.arguments["port"]; - var protectionSpace = ProtectionSpace( - host: host, protocol: protocol, realm: realm, port: port); - var challenge = ClientCertChallenge(protectionSpace: protectionSpace); - if (_webview != null && _webview!.onReceivedClientCertRequest != null) - return (await _webview!.onReceivedClientCertRequest!(this, challenge)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.onReceivedClientCertRequest(challenge)) - ?.toMap(); + if ((_webview != null && _webview!.onReceivedClientCertRequest != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + ClientCertChallenge challenge = ClientCertChallenge.fromMap(arguments)!; + + if (_webview != null && _webview!.onReceivedClientCertRequest != null) + return (await _webview!.onReceivedClientCertRequest!(this, challenge)) + ?.toMap(); + else + return (await _inAppBrowser!.onReceivedClientCertRequest(challenge)) + ?.toMap(); + } break; case "onFindResultReceived": - int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; - int numberOfMatches = call.arguments["numberOfMatches"]; - bool isDoneCounting = call.arguments["isDoneCounting"]; - if (_webview != null && _webview!.onFindResultReceived != null) - _webview!.onFindResultReceived!( - this, activeMatchOrdinal, numberOfMatches, isDoneCounting); - else if (_inAppBrowser != null) - _inAppBrowser!.onFindResultReceived( - activeMatchOrdinal, numberOfMatches, isDoneCounting); + if ((_webview != null && _webview!.onFindResultReceived != null) || _inAppBrowser != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + if (_webview != null && _webview!.onFindResultReceived != null) + _webview!.onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + else + _inAppBrowser!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } break; case "onPermissionRequest": - String origin = call.arguments["origin"]; - List resources = call.arguments["resources"].cast(); - if (_webview != null && _webview!.androidOnPermissionRequest != null) - return (await _webview!.androidOnPermissionRequest!( - this, origin, resources)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser!.androidOnPermissionRequest( - origin, resources)) - ?.toMap(); + if ((_webview != null && _webview!.androidOnPermissionRequest != null) || _inAppBrowser != null) { + String origin = call.arguments["origin"]; + List resources = call.arguments["resources"].cast(); + if (_webview != null && _webview!.androidOnPermissionRequest != null) + return (await _webview!.androidOnPermissionRequest!( + this, origin, resources)) + ?.toMap(); + else + return (await _inAppBrowser!.androidOnPermissionRequest( + origin, resources)) + ?.toMap(); + } break; case "onUpdateVisitedHistory": - String? url = call.arguments["url"]; - bool? androidIsReload = call.arguments["androidIsReload"]; - if (_webview != null && _webview!.onUpdateVisitedHistory != null) - _webview!.onUpdateVisitedHistory!(this, url, androidIsReload); - else if (_inAppBrowser != null) - _inAppBrowser!.onUpdateVisitedHistory(url, androidIsReload); - return null; + if ((_webview != null && _webview!.onUpdateVisitedHistory != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + bool? androidIsReload = call.arguments["androidIsReload"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onUpdateVisitedHistory != null) + _webview!.onUpdateVisitedHistory!(this, uri, androidIsReload); + else + _inAppBrowser!.onUpdateVisitedHistory(uri, androidIsReload); + } + break; case "onWebContentProcessDidTerminate": if (_webview != null && _webview!.iosOnWebContentProcessDidTerminate != null) @@ -628,10 +504,14 @@ class InAppWebViewController { _inAppBrowser!.iosOnWebContentProcessDidTerminate(); break; case "onPageCommitVisible": - String? url = call.arguments["url"]; - if (_webview != null && _webview!.onPageCommitVisible != null) - _webview!.onPageCommitVisible!(this, url); - else if (_inAppBrowser != null) _inAppBrowser!.onPageCommitVisible(url); + if ((_webview != null && _webview!.onPageCommitVisible != null) || _inAppBrowser != null) { + String? url = call.arguments["url"]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onPageCommitVisible != null) + _webview!.onPageCommitVisible!(this, uri); + else + _inAppBrowser!.onPageCommitVisible(uri); + } break; case "onDidReceiveServerRedirectForProvisionalNavigation": if (_webview != null && @@ -642,69 +522,45 @@ class InAppWebViewController { _inAppBrowser!.iosOnDidReceiveServerRedirectForProvisionalNavigation(); break; case "onNavigationResponse": - String? url = call.arguments["url"]; - bool isForMainFrame = call.arguments["isForMainFrame"]; - bool canShowMIMEType = call.arguments["canShowMIMEType"]; - int expectedContentLength = call.arguments["expectedContentLength"]; - String? mimeType = call.arguments["mimeType"]; - String? suggestedFilename = call.arguments["suggestedFilename"]; - String? textEncodingName = call.arguments["textEncodingName"]; + if ((_webview != null && _webview!.iosOnNavigationResponse != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + IOSWKNavigationResponse iosOnNavigationResponse = IOSWKNavigationResponse.fromMap(arguments)!; - IOSNavigationResponse iosOnNavigationResponse = - IOSNavigationResponse( - url: url, - isForMainFrame: isForMainFrame, - canShowMIMEType: canShowMIMEType, - expectedContentLength: expectedContentLength, - mimeType: mimeType, - suggestedFilename: suggestedFilename, - textEncodingName: textEncodingName - ); - - if (_webview != null && _webview!.iosOnNavigationResponse != null) - return (await _webview!.iosOnNavigationResponse!( - this, iosOnNavigationResponse)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser! - .iosOnNavigationResponse(iosOnNavigationResponse)) - ?.toMap(); + if (_webview != null && _webview!.iosOnNavigationResponse != null) + return (await _webview!.iosOnNavigationResponse!( + this, iosOnNavigationResponse)) + ?.toMap(); + else + return (await _inAppBrowser! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toMap(); + } break; case "shouldAllowDeprecatedTLS": - String host = call.arguments["host"]; - String protocol = call.arguments["protocol"]; - String? realm = call.arguments["realm"]; - int? port = call.arguments["port"]; - int previousFailureCount = call.arguments["previousFailureCount"]; - var protectionSpace = ProtectionSpace( - host: host, protocol: protocol, realm: realm, port: port); - var challenge = URLAuthenticationChallenge( - previousFailureCount: previousFailureCount, - protectionSpace: protectionSpace); + if ((_webview != null && _webview!.iosShouldAllowDeprecatedTLS != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + URLAuthenticationChallenge challenge = URLAuthenticationChallenge.fromMap(arguments)!; - if (_webview != null && _webview!.iosShouldAllowDeprecatedTLS != null) - return (await _webview!.iosShouldAllowDeprecatedTLS!( - this, challenge)) - ?.toMap(); - else if (_inAppBrowser != null) - return (await _inAppBrowser! - .iosShouldAllowDeprecatedTLS(challenge)) - ?.toMap(); + if (_webview != null && _webview!.iosShouldAllowDeprecatedTLS != null) + return (await _webview!.iosShouldAllowDeprecatedTLS!( + this, challenge)) + ?.toMap(); + else + return (await _inAppBrowser! + .iosShouldAllowDeprecatedTLS(challenge)) + ?.toMap(); + } break; case "onLongPressHitTestResult": - Map? hitTestResultMap = - call.arguments["hitTestResult"]; - InAppWebViewHitTestResultType? type = - InAppWebViewHitTestResultType.fromValue( - hitTestResultMap?["type"]?.toInt()); - String? extra = hitTestResultMap?["extra"]; - InAppWebViewHitTestResult hitTestResult = - InAppWebViewHitTestResult(type: type, extra: extra); + if ((_webview != null && _webview!.onLongPressHitTestResult != null) || _inAppBrowser != null) { + Map arguments = call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = InAppWebViewHitTestResult.fromMap(arguments)!; - if (_webview != null && _webview!.onLongPressHitTestResult != null) - _webview!.onLongPressHitTestResult!(this, hitTestResult); - else if (_inAppBrowser != null) - _inAppBrowser!.onLongPressHitTestResult(hitTestResult); + if (_webview != null && _webview!.onLongPressHitTestResult != null) + _webview!.onLongPressHitTestResult!(this, hitTestResult); + else + _inAppBrowser!.onLongPressHitTestResult(hitTestResult); + } break; case "onCreateContextMenu": ContextMenu? contextMenu; @@ -715,14 +571,8 @@ class InAppWebViewController { } if (contextMenu != null && contextMenu.onCreateContextMenu != null) { - Map? hitTestResultMap = - call.arguments["hitTestResult"]; - InAppWebViewHitTestResultType? type = - InAppWebViewHitTestResultType.fromValue( - hitTestResultMap?["type"].toInt()); - String? extra = hitTestResultMap?["extra"]; - InAppWebViewHitTestResult hitTestResult = - InAppWebViewHitTestResult(type: type, extra: extra); + Map arguments = call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = InAppWebViewHitTestResult.fromMap(arguments)!; contextMenu.onCreateContextMenu!(hitTestResult); } @@ -788,199 +638,82 @@ class InAppWebViewController { switch (handlerName) { case "onLoadResource": - Map argMap = args[0]; - String? initiatorType = argMap["initiatorType"]; - String? url = argMap["name"]; - double? startTime = argMap["startTime"] is int - ? argMap["startTime"].toDouble() - : argMap["startTime"]; - double? duration = argMap["duration"] is int - ? argMap["duration"].toDouble() - : argMap["duration"]; + if ((_webview != null && _webview!.onLoadResource != null) || _inAppBrowser != null) { + Map arguments = args[0].cast(); + arguments["startTime"] = arguments["startTime"] is int + ? arguments["startTime"].toDouble() + : arguments["startTime"]; + arguments["duration"] = arguments["duration"] is int + ? arguments["duration"].toDouble() + : arguments["duration"]; - var response = new LoadedResource( - initiatorType: initiatorType, - url: url, - startTime: startTime, - duration: duration); + var response = LoadedResource.fromMap(arguments)!; - if (_webview != null && _webview!.onLoadResource != null) - _webview!.onLoadResource!(this, response); - else if (_inAppBrowser != null) - _inAppBrowser!.onLoadResource(response); + if (_webview != null && _webview!.onLoadResource != null) + _webview!.onLoadResource!(this, response); + else + _inAppBrowser!.onLoadResource(response); + } return null; case "shouldInterceptAjaxRequest": - Map argMap = args[0]; - dynamic data = argMap["data"]; - String? method = argMap["method"]; - String? url = argMap["url"]; - bool? isAsync = argMap["isAsync"]; - String? user = argMap["user"]; - String? password = argMap["password"]; - bool? withCredentials = argMap["withCredentials"]; - AjaxRequestHeaders headers = AjaxRequestHeaders(argMap["headers"] ?? {}); - String? responseType = argMap["responseType"]; + if ((_webview != null && _webview!.shouldInterceptAjaxRequest != null) || _inAppBrowser != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; - var request = new AjaxRequest( - data: data, - method: method, - url: url, - isAsync: isAsync, - user: user, - password: password, - withCredentials: withCredentials, - headers: headers, - responseType: responseType); - - if (_webview != null && _webview!.shouldInterceptAjaxRequest != null) - return jsonEncode( - await _webview!.shouldInterceptAjaxRequest!(this, request)); - else if (_inAppBrowser != null) - return jsonEncode( - await _inAppBrowser!.shouldInterceptAjaxRequest(request)); + if (_webview != null && _webview!.shouldInterceptAjaxRequest != null) + return jsonEncode( + await _webview!.shouldInterceptAjaxRequest!(this, request)); + else + return jsonEncode( + await _inAppBrowser!.shouldInterceptAjaxRequest(request)); + } return null; case "onAjaxReadyStateChange": - Map argMap = args[0]; - dynamic data = argMap["data"]; - String? method = argMap["method"]; - String? url = argMap["url"]; - bool? isAsync = argMap["isAsync"]; - String? user = argMap["user"]; - String? password = argMap["password"]; - bool? withCredentials = argMap["withCredentials"]; - AjaxRequestHeaders headers = AjaxRequestHeaders(argMap["headers"] ?? {}); - int? readyState = argMap["readyState"]; - int? status = argMap["status"]; - String? responseURL = argMap["responseURL"]; - String? responseType = argMap["responseType"]; - dynamic response = argMap["response"]; - String? responseText = argMap["responseText"]; - String? responseXML = argMap["responseXML"]; - String? statusText = argMap["statusText"]; - Map? responseHeaders = argMap["responseHeaders"]; + if ((_webview != null && _webview!.onAjaxReadyStateChange != null) || _inAppBrowser != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; - var request = new AjaxRequest( - data: data, - method: method, - url: url, - isAsync: isAsync, - user: user, - password: password, - withCredentials: withCredentials, - headers: headers, - readyState: AjaxRequestReadyState.fromValue(readyState), - status: status, - responseURL: responseURL, - responseType: responseType, - response: response, - responseText: responseText, - responseXML: responseXML, - statusText: statusText, - responseHeaders: responseHeaders); - - if (_webview != null && _webview!.onAjaxReadyStateChange != null) - return jsonEncode( - await _webview!.onAjaxReadyStateChange!(this, request)); - else if (_inAppBrowser != null) - return jsonEncode( - await _inAppBrowser!.onAjaxReadyStateChange(request)); + if (_webview != null && _webview!.onAjaxReadyStateChange != null) + return jsonEncode( + await _webview!.onAjaxReadyStateChange!(this, request)); + else + return jsonEncode( + await _inAppBrowser!.onAjaxReadyStateChange(request)); + } return null; case "onAjaxProgress": - Map argMap = args[0]; - dynamic data = argMap["data"]; - String? method = argMap["method"]; - String? url = argMap["url"]; - bool? isAsync = argMap["isAsync"]; - String? user = argMap["user"]; - String? password = argMap["password"]; - bool? withCredentials = argMap["withCredentials"]; - AjaxRequestHeaders headers = AjaxRequestHeaders(argMap["headers"] ?? {}); - int? readyState = argMap["readyState"]; - int? status = argMap["status"]; - String? responseURL = argMap["responseURL"]; - String? responseType = argMap["responseType"]; - dynamic response = argMap["response"]; - String? responseText = argMap["responseText"]; - String? responseXML = argMap["responseXML"]; - String? statusText = argMap["statusText"]; - Map? responseHeaders = argMap["responseHeaders"]; - Map? eventMap = argMap["event"]; + if ((_webview != null && _webview!.onAjaxProgress != null) || _inAppBrowser != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; - AjaxRequestEvent event = AjaxRequestEvent( - lengthComputable: eventMap?["lengthComputable"], - loaded: eventMap?["loaded"], - total: eventMap?["total"], - type: AjaxRequestEventType.fromValue(eventMap?["type"])); - - var request = new AjaxRequest( - data: data, - method: method, - url: url, - isAsync: isAsync, - user: user, - password: password, - withCredentials: withCredentials, - headers: headers, - readyState: AjaxRequestReadyState.fromValue(readyState), - status: status, - responseURL: responseURL, - responseType: responseType, - response: response, - responseText: responseText, - responseXML: responseXML, - statusText: statusText, - responseHeaders: responseHeaders, - event: event); - - if (_webview != null && _webview!.onAjaxProgress != null) - return jsonEncode(await _webview!.onAjaxProgress!(this, request)); - else if (_inAppBrowser != null) - return jsonEncode(await _inAppBrowser!.onAjaxProgress(request)); + if (_webview != null && _webview!.onAjaxProgress != null) + return jsonEncode(await _webview!.onAjaxProgress!(this, request)); + else + return jsonEncode(await _inAppBrowser!.onAjaxProgress(request)); + } return null; case "shouldInterceptFetchRequest": - Map argMap = args[0]; - String? url = argMap["url"]; - String? method = argMap["method"]; - Map? headers = argMap["headers"]; - headers = headers?.cast(); - Uint8List? body = argMap["body"] != null ? Uint8List.fromList(argMap["body"].cast()) : null; - String? mode = argMap["mode"]; - FetchRequestCredential? credentials = - FetchRequest.fromMap(argMap["credentials"]); - String? cache = argMap["cache"]; - String? redirect = argMap["redirect"]; - String? referrer = argMap["referrer"]; - String? referrerPolicy = argMap["referrerPolicy"]; - String? integrity = argMap["integrity"]; - bool? keepalive = argMap["keepalive"]; + if ((_webview != null && _webview!.shouldInterceptFetchRequest != null) || _inAppBrowser != null) { + Map arguments = args[0].cast(); + FetchRequest request = FetchRequest.fromMap(arguments)!; - var request = new FetchRequest( - url: url, - method: method, - headers: headers as Map?, - body: body, - mode: mode, - credentials: credentials, - cache: cache, - redirect: redirect, - referrer: referrer, - referrerPolicy: referrerPolicy, - integrity: integrity, - keepalive: keepalive); - - if (_webview != null && - _webview!.shouldInterceptFetchRequest != null) - return jsonEncode( - await _webview!.shouldInterceptFetchRequest!(this, request)); - else if (_inAppBrowser != null) - return jsonEncode( - await _inAppBrowser!.shouldInterceptFetchRequest(request)); + if (_webview != null && _webview!.shouldInterceptFetchRequest != null) + return jsonEncode( + await _webview!.shouldInterceptFetchRequest!(this, request)); + else + return jsonEncode( + await _inAppBrowser!.shouldInterceptFetchRequest(request)); + } return null; case "onPrint": - String? url = args[0]; - if (_webview != null && _webview!.onPrint != null) - _webview!.onPrint!(this, url); - else if (_inAppBrowser != null) _inAppBrowser!.onPrint(url); + if ((_webview != null && _webview!.onPrint != null) || _inAppBrowser != null) { + String? url = args[0]; + Uri? uri = url != null ? Uri.parse(url) : null; + if (_webview != null && _webview!.onPrint != null) + _webview!.onPrint!(this, uri); + else + _inAppBrowser!.onPrint(uri); + } return null; case "onWindowFocus": if (_webview != null && _webview!.onWindowFocus != null) @@ -1016,9 +749,10 @@ class InAppWebViewController { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getUrl() /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/1415005-url - Future getUrl() async { + Future getUrl() async { Map args = {}; - return await _channel.invokeMethod('getUrl', args); + String? url = await _channel.invokeMethod('getUrl', args); + return url != null ? Uri.parse(url) : null; } ///Gets the title for the current page. @@ -1051,8 +785,8 @@ class InAppWebViewController { String? html; InAppWebViewGroupOptions? options = await getOptions(); - if (options != null && options.crossPlatform != null && - options.crossPlatform!.javaScriptEnabled == true) { + if (options != null && + options.crossPlatform.javaScriptEnabled == true) { html = await evaluateJavascript( source: "window.document.getElementsByTagName('html')[0].outerHTML;"); if (html != null && html.isNotEmpty) return html; @@ -1063,8 +797,8 @@ class InAppWebViewController { return html; } - if (webviewUrl.startsWith("file:///")) { - var assetPathSplitted = webviewUrl.split("/flutter_assets/"); + if (webviewUrl.isScheme("file")) { + var assetPathSplitted = webviewUrl.toString().split("/flutter_assets/"); var assetPath = assetPathSplitted[assetPathSplitted.length - 1]; try { var bytes = await rootBundle.load(assetPath); @@ -1072,9 +806,8 @@ class InAppWebViewController { } catch (e) {} } else { HttpClient client = new HttpClient(); - var url = Uri.parse(webviewUrl); try { - var htmlRequest = await client.getUrl(url); + var htmlRequest = await client.getUrl(webviewUrl); html = await (await htmlRequest.close()).transform(Utf8Decoder()).join(); } catch (e) { @@ -1096,9 +829,6 @@ class InAppWebViewController { return favicons; } - var url = (webviewUrl.startsWith("file:///")) - ? Uri.file(webviewUrl) - : Uri.parse(webviewUrl); String? manifestUrl; var html = await getHtml(); @@ -1107,14 +837,14 @@ class InAppWebViewController { } var assetPathBase; - if (webviewUrl.startsWith("file:///")) { - var assetPathSplitted = webviewUrl.split("/flutter_assets/"); + if (webviewUrl.isScheme("file")) { + var assetPathSplitted = webviewUrl.toString().split("/flutter_assets/"); assetPathBase = assetPathSplitted[0] + "/flutter_assets/"; } InAppWebViewGroupOptions? options = await getOptions(); - if (options != null && options.crossPlatform != null && - options.crossPlatform!.javaScriptEnabled == true) { + if (options != null && + options.crossPlatform.javaScriptEnabled == true) { List> links = (await evaluateJavascript(source: """ (function() { var linkNodes = document.head.getElementsByTagName("link"); @@ -1150,22 +880,23 @@ class InAppWebViewController { manifestUrl = manifestUrl.substring(1); } manifestUrl = ((assetPathBase == null) - ? url.scheme + "://" + url.host + "/" + ? webviewUrl.scheme + "://" + webviewUrl.host + "/" : assetPathBase) + manifestUrl; } continue; } - favicons.addAll(_createFavicons(url, assetPathBase, link["href"], + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, link["href"], link["rel"], link["sizes"], false)); } } // try to get /favicon.ico try { - var faviconUrl = url.scheme + "://" + url.host + "/favicon.ico"; - await client.headUrl(Uri.parse(faviconUrl)); - favicons.add(Favicon(url: faviconUrl, rel: "shortcut icon")); + var faviconUrl = webviewUrl.scheme + "://" + webviewUrl.host + "/favicon.ico"; + var faviconUri = Uri.parse(faviconUrl); + await client.headUrl(faviconUri); + favicons.add(Favicon(url: faviconUri, rel: "shortcut icon")); } catch (e) { print("/favicon.ico file not found: " + e.toString()); // print(stacktrace); @@ -1176,7 +907,7 @@ class InAppWebViewController { HttpClientResponse? manifestResponse; bool manifestFound = false; if (manifestUrl == null) { - manifestUrl = url.scheme + "://" + url.host + "/manifest.json"; + manifestUrl = webviewUrl.scheme + "://" + webviewUrl.host + "/manifest.json"; } try { manifestRequest = await client.getUrl(Uri.parse(manifestUrl)); @@ -1193,7 +924,7 @@ class InAppWebViewController { json.decode(await manifestResponse!.transform(Utf8Decoder()).join()); if (manifest.containsKey("icons")) { for (Map icon in manifest["icons"]) { - favicons.addAll(_createFavicons(url, assetPathBase, icon["src"], + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, icon["src"], icon["rel"], icon["sizes"], true)); } } @@ -1234,18 +965,18 @@ class InAppWebViewController { int width = int.parse(size.split("x")[0]); int height = int.parse(size.split("x")[1]); favicons - .add(Favicon(url: urlIcon, rel: rel, width: width, height: height)); + .add(Favicon(url: Uri.parse(urlIcon), rel: rel, width: width, height: height)); } } else { - favicons.add(Favicon(url: urlIcon, rel: rel, width: null, height: null)); + favicons.add(Favicon(url: Uri.parse(urlIcon), rel: rel, width: null, height: null)); } return favicons; } - ///Loads the given [url] with optional [headers] specified as a map from name to value. + ///Loads the given [urlRequest]. /// - ///[iosAllowingReadAccessTo], used in combination with [url] (using the `file://` scheme), + ///[iosAllowingReadAccessTo], used in combination with [urlRequest] (using the `file://` scheme), ///is an iOS-specific argument that represents the URL from which to read the web content. ///This URL must be a file-based URL (using the `file://` scheme). ///Specify the same value as the URL parameter to prevent WebView from reading any other content. @@ -1256,26 +987,29 @@ class InAppWebViewController { ///**Official iOS API**: ///- https://developer.apple.com/documentation/webkit/wkwebview/1414954-load ///- if [iosAllowingReadAccessTo] is used, https://developer.apple.com/documentation/webkit/wkwebview/1414973-loadfileurl - Future loadUrl( - {required String url, Map headers = const {}, String? iosAllowingReadAccessTo}) async { - assert(url.isNotEmpty); - assert(iosAllowingReadAccessTo == null || iosAllowingReadAccessTo.startsWith("file://")); + Future loadUrl({required URLRequest urlRequest, Uri? iosAllowingReadAccessTo}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert(iosAllowingReadAccessTo == null || iosAllowingReadAccessTo.isScheme("file")); Map args = {}; - args.putIfAbsent('url', () => url); - args.putIfAbsent('headers', () => headers); - args.putIfAbsent('iosAllowingReadAccessTo', () => iosAllowingReadAccessTo); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent('allowingReadAccessTo', () => iosAllowingReadAccessTo.toString()); await _channel.invokeMethod('loadUrl', args); } - ///Loads the given [url] with [postData] using `POST` method into this WebView. + ///Loads the given [url] with [postData] (x-www-form-urlencoded) using `POST` method into this WebView. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#postUrl(java.lang.String,%20byte[]) - Future postUrl( - {required String url, required Uint8List postData}) async { - assert(url.isNotEmpty); + /// + ///Example + ///```dart + ///var postData = Uint8List.fromList(utf8.encode("firstname=Foo&surname=Bar")); + ///controller.postUrl(url: Uri.parse("https://www.example.com/"), postData: postData); + ///``` + Future postUrl({required Uri url, required Uint8List postData}) async { + assert(url.toString().isNotEmpty); Map args = {}; - args.putIfAbsent('url', () => url); + args.putIfAbsent('url', () => url.toString()); args.putIfAbsent('postData', () => postData); await _channel.invokeMethod('postUrl', args); } @@ -1297,18 +1031,18 @@ class InAppWebViewController { {required String data, String mimeType = "text/html", String encoding = "utf8", - String baseUrl = "about:blank", - String androidHistoryUrl = "about:blank"}) async { + Uri? baseUrl, + Uri? androidHistoryUrl}) async { Map args = {}; args.putIfAbsent('data', () => data); args.putIfAbsent('mimeType', () => mimeType); args.putIfAbsent('encoding', () => encoding); - args.putIfAbsent('baseUrl', () => baseUrl); - args.putIfAbsent('historyUrl', () => androidHistoryUrl); + args.putIfAbsent('baseUrl', () => baseUrl ?? Uri.parse("about:blank")); + args.putIfAbsent('historyUrl', () => androidHistoryUrl ?? Uri.parse("about:blank")); await _channel.invokeMethod('loadData', args); } - ///Loads the given [assetFilePath] with optional [headers] specified as a map from name to value. + ///Loads the given [assetFilePath]. /// ///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! /// @@ -1331,19 +1065,16 @@ class InAppWebViewController { /// ///... ///``` - ///Example of a `main.dart` file: + ///Example: ///```dart ///... - ///inAppBrowser.loadFile("assets/index.html"); + ///controller.loadFile(assetFilePath: "assets/index.html"); ///... ///``` - Future loadFile( - {required String assetFilePath, - Map headers = const {}}) async { + Future loadFile({required String assetFilePath}) async { assert(assetFilePath.isNotEmpty); Map args = {}; - args.putIfAbsent('url', () => assetFilePath); - args.putIfAbsent('headers', () => headers); + args.putIfAbsent('assetFilePath', () => assetFilePath); await _channel.invokeMethod('loadFile', args); } @@ -1443,7 +1174,7 @@ class InAppWebViewController { ///Evaluates JavaScript [source] code into the WebView and returns the result of the evaluation. /// ///[contentWorld], on iOS, it represents the namespace in which to evaluate the JavaScript [source] code. - ///Instead, on Android, it will run the [source] code into an iframe. + ///Instead, on Android, it will run the [source] code into an iframe, using `eval(source);` to get and return the result. ///This parameter doesn’t apply to changes you make to the underlying web content, such as the document’s DOM structure. ///Those changes remain visible to all scripts, regardless of which content world you specify. ///For more information about content worlds, see [ContentWorld]. @@ -1462,7 +1193,7 @@ class InAppWebViewController { Future evaluateJavascript({required String source, ContentWorld? contentWorld}) async { Map args = {}; args.putIfAbsent('source', () => source); - args.putIfAbsent('contentWorld', () => contentWorld?.name); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); var data = await _channel.invokeMethod('evaluateJavascript', args); if (data != null && defaultTargetPlatform == TargetPlatform.android) data = json.decode(data); return data; @@ -1476,9 +1207,10 @@ class InAppWebViewController { ///because, in these events, the [WebView] is not ready to handle it yet. ///Instead, you should call this method, for example, inside the [WebView.onLoadStop] event or in any other events ///where you know the page is ready "enough". - Future injectJavascriptFileFromUrl({required String urlFile, ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + Future injectJavascriptFileFromUrl({required Uri urlFile, ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); Map args = {}; - args.putIfAbsent('urlFile', () => urlFile); + args.putIfAbsent('urlFile', () => urlFile.toString()); args.putIfAbsent('scriptHtmlTagAttributes', () => scriptHtmlTagAttributes?.toMap()); await _channel.invokeMethod('injectJavascriptFileFromUrl', args); } @@ -1515,9 +1247,10 @@ class InAppWebViewController { ///because, in these events, the [WebView] is not ready to handle it yet. ///Instead, you should call this method, for example, inside the [WebView.onLoadStop] event or in any other events ///where you know the page is ready "enough". - Future injectCSSFileFromUrl({required String urlFile, CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + Future injectCSSFileFromUrl({required Uri urlFile, CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); Map args = {}; - args.putIfAbsent('urlFile', () => urlFile); + args.putIfAbsent('urlFile', () => urlFile.toString()); args.putIfAbsent('cssLinkHtmlTagAttributes', () => cssLinkHtmlTagAttributes?.toMap()); await _channel.invokeMethod('injectCSSFileFromUrl', args); } @@ -1578,7 +1311,7 @@ class InAppWebViewController { /// """); ///``` /// - ///Forbidden names for JavaScript handlers are defined in [javaScriptHandlerForbiddenNames]. + ///Forbidden names for JavaScript handlers are defined in [_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES]. /// ///**NOTE**: This method should be called, for example, in the [WebView.onWebViewCreated] or [WebView.onLoadStart] events or, at least, ///before you know that your JavaScript code will call the `window.flutter_inappwebview.callHandler` method, @@ -1586,7 +1319,7 @@ class InAppWebViewController { void addJavaScriptHandler( {required String handlerName, required JavaScriptHandlerCallback callback}) { - assert(!javaScriptHandlerForbiddenNames.contains(handlerName)); + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName)); this.javaScriptHandlersMap[handlerName] = (callback); } @@ -1661,9 +1394,9 @@ class InAppWebViewController { for (var i = 0; i < historyListMap.length; i++) { LinkedHashMap historyItem = historyListMap[i]; historyList.add(WebHistoryItem( - originalUrl: historyItem["originalUrl"], + originalUrl: historyItem["originalUrl"] != null ? Uri.parse(historyItem["originalUrl"]) : null, title: historyItem["title"], - url: historyItem["url"], + url: historyItem["url"] != null ? Uri.parse(historyItem["url"]) : null, index: i, offset: i - currentIndex)); } @@ -1676,7 +1409,7 @@ class InAppWebViewController { await _channel.invokeMethod('clearCache', args); } - ///Finds all instances of find on the page and highlights them. Notifies [onFindResultReceived] listener. + ///Finds all instances of find on the page and highlights them. Notifies [WebView.onFindResultReceived] listener. /// ///[find] represents the string to find. /// @@ -1691,7 +1424,7 @@ class InAppWebViewController { await _channel.invokeMethod('findAllAsync', args); } - ///Highlights and scrolls to the next match found by [findAllAsync()]. Notifies [onFindResultReceived] listener. + ///Highlights and scrolls to the next match found by [findAllAsync()]. Notifies [WebView.onFindResultReceived] listener. /// ///[forward] represents the direction to search. /// @@ -1905,7 +1638,7 @@ class InAppWebViewController { await _channel.invokeMethod('requestFocusNodeHref', args); return result != null ? RequestFocusNodeHrefResult( - url: result['url'], + url: result['url'] != null ? Uri.parse(result['url']) : null, title: result['title'], src: result['src'], ) @@ -1923,7 +1656,7 @@ class InAppWebViewController { await _channel.invokeMethod('requestImageRef', args); return result != null ? RequestImageRefResult( - url: result['url'], + url: result['url'] != null ? Uri.parse(result['url']) : null, ) : null; } @@ -2011,7 +1744,7 @@ class InAppWebViewController { var colorValue = metaTagThemeColor.content; - return colorValue != null ? Util.convertColorFromStringRepresentation(colorValue) : null; + return colorValue != null ? UtilColor.fromStringRepresentation(colorValue) : null; } ///Returns the scrolled left position of the current WebView. @@ -2039,60 +1772,22 @@ class InAppWebViewController { ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getCertificate() Future getCertificate() async { Map args = {}; - Map? sslCertificateMap = (await _channel.invokeMethod('getCertificate', args)) ?.cast(); - - if (sslCertificateMap != null) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - try { - X509Certificate x509certificate = X509Certificate.fromData( - data: sslCertificateMap["x509Certificate"]); - return SslCertificate( - issuedBy: SslCertificateDName( - CName: x509certificate.issuer( - dn: ASN1DistinguishedNames.COMMON_NAME) ?? - "", - DName: x509certificate.issuerDistinguishedName ?? "", - OName: x509certificate.issuer( - dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? - "", - UName: x509certificate.issuer( - dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? - ""), - issuedTo: SslCertificateDName( - CName: x509certificate.subject( - dn: ASN1DistinguishedNames.COMMON_NAME) ?? - "", - DName: x509certificate.subjectDistinguishedName ?? "", - OName: x509certificate.subject( - dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? - "", - UName: x509certificate.subject( - dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? - ""), - validNotAfterDate: x509certificate.notAfter, - validNotBeforeDate: x509certificate.notBefore, - x509Certificate: x509certificate, - ); - } catch (e, stacktrace) { - print(e); - print(stacktrace); - return null; - } - } else { - return SslCertificate.fromMap(sslCertificateMap); - } - } - - return null; + return SslCertificate.fromMap(sslCertificateMap); } ///Injects the specified [userScript] into the webpage’s content. /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537448-adduserscript - Future addUserScript(UserScript userScript) async { + Future addUserScript({required UserScript userScript}) async { + assert(_webview?.windowId == null || !Platform.isIOS); + Map args = {}; args.putIfAbsent('userScript', () => userScript.toMap()); if (!_userScripts.contains(userScript)) { @@ -2102,16 +1797,28 @@ class InAppWebViewController { } ///Injects the [userScripts] into the webpage’s content. - Future addUserScripts(List userScripts) async { + /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + Future addUserScripts({required List userScripts}) async { + assert(_webview?.windowId == null || !Platform.isIOS); + for (var i = 0; i < userScripts.length; i++) { - await addUserScript(userScripts[i]); + await addUserScript(userScript: userScripts[i]); } } ///Removes the specified [userScript] from the webpage’s content. ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. ///Returns `true` if [userScript] was in the list, `false` otherwise. - Future removeUserScript(UserScript userScript) async { + /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + Future removeUserScript({required UserScript userScript}) async { + assert(_webview?.windowId == null || !Platform.isIOS); + var index = _userScripts.indexOf(userScript); if (index == -1) { return false; @@ -2119,24 +1826,51 @@ class InAppWebViewController { _userScripts.remove(userScript); Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); args.putIfAbsent('index', () => index); await _channel.invokeMethod('removeUserScript', args); return true; } + ///Removes all the [UserScript]s with [groupName] as group name from the webpage’s content. + ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. + /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + Future removeUserScriptsByGroupName({required String groupName}) async { + assert(_webview?.windowId == null || !Platform.isIOS); + + Map args = {}; + args.putIfAbsent('groupName', () => groupName); + await _channel.invokeMethod('removeUserScriptsByGroupName', args); + } + ///Removes the [userScripts] from the webpage’s content. ///User scripts already loaded into the webpage's content cannot be removed. This will have effect only on the next page load. - Future removeUserScripts(List userScripts) async { + /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + Future removeUserScripts({required List userScripts}) async { + assert(_webview?.windowId == null || !Platform.isIOS); + for (var i = 0; i < userScripts.length; i++) { - await removeUserScript(userScripts[i]); + await removeUserScript(userScript: userScripts[i]); } } ///Removes all the user scripts from the webpage’s content. /// + ///**NOTE for iOS**: this method will throw an error if the [WebView.windowId] has been set. + ///There isn't any way to add/remove user scripts specific to iOS window WebViews. + ///This is a limitation of the native iOS WebKit APIs. + /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1536540-removealluserscripts Future removeAllUserScripts() async { + assert(_webview?.windowId == null || !Platform.isIOS); + _userScripts.clear(); Map args = {}; await _channel.invokeMethod('removeAllUserScripts', args); @@ -2167,11 +1901,13 @@ class InAppWebViewController { ///**NOTE for Android**: available only on Android 21+. /// ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3656441-callasyncjavascript - Future callAsyncJavaScript({required String functionBody, Map arguments = const {}, ContentWorld? contentWorld}) async { + Future callAsyncJavaScript({required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld}) async { Map args = {}; args.putIfAbsent('functionBody', () => functionBody); args.putIfAbsent('arguments', () => arguments); - args.putIfAbsent('contentWorld', () => contentWorld?.name); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); var data = await _channel.invokeMethod('callAsyncJavaScript', args); if (data == null) { return null; @@ -2211,6 +1947,16 @@ class InAppWebViewController { return await _channel.invokeMethod('saveWebArchive', args); } + ///Indicates whether the webpage context is capable of using features that require [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + ///This is implemented using Javascript (see [window.isSecureContext](https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext)). + /// + ///**NOTE for Android**: available Android 21.0+. + Future isSecureContext() async { + Map args = {}; + return await _channel + .invokeMethod('isSecureContext', args); + } + ///Gets the default user agent. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebSettings#getDefaultUserAgent(android.content.Context) @@ -2218,266 +1964,4 @@ class InAppWebViewController { Map args = {}; return await _staticChannel.invokeMethod('getDefaultUserAgent', args); } -} - -///Class represents the Android controller that contains only android-specific methods for the WebView. -class AndroidInAppWebViewController { - late InAppWebViewController _controller; - - AndroidInAppWebViewController(InAppWebViewController controller) { - this._controller = controller; - } - - ///Starts Safe Browsing initialization. - /// - ///URL loads are not guaranteed to be protected by Safe Browsing until after the this method returns true. - ///Safe Browsing is not fully supported on all devices. For those devices this method will returns false. - /// - ///This should not be called if Safe Browsing has been disabled by manifest tag - ///or [AndroidInAppWebViewOptions.safeBrowsingEnabled]. This prepares resources used for Safe Browsing. - /// - ///**NOTE**: available only on Android 27+. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#startSafeBrowsing(android.content.Context,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E) - Future startSafeBrowsing() async { - Map args = {}; - return await _controller._channel.invokeMethod('startSafeBrowsing', args); - } - - ///Clears the SSL preferences table stored in response to proceeding with SSL certificate errors. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearSslPreferences() - Future clearSslPreferences() async { - Map args = {}; - await _controller._channel.invokeMethod('clearSslPreferences', args); - } - - ///Does a best-effort attempt to pause any processing that can be paused safely, such as animations and geolocation. Note that this call does not pause JavaScript. - ///To pause JavaScript globally, use [pauseTimers()]. To resume WebView, call [resume()]. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#onPause() - Future pause() async { - Map args = {}; - await _controller._channel.invokeMethod('pause', args); - } - - ///Resumes a WebView after a previous call to [pause()]. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#onResume() - Future resume() async { - Map args = {}; - await _controller._channel.invokeMethod('resume', args); - } - - ///Gets the URL that was originally requested for the current page. - ///This is not always the same as the URL passed to [InAppWebView.onLoadStarted] because although the load for that URL has begun, - ///the current page may not have changed. Also, there may have been redirects resulting in a different URL to that originally requested. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getOriginalUrl() - Future getOriginalUrl() async { - Map args = {}; - return await _controller._channel.invokeMethod('getOriginalUrl', args); - } - - ///Scrolls the contents of this WebView down by half the page size. - ///Returns `true` if the page was scrolled. - /// - ///[bottom] `true` to jump to bottom of page. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#pageDown(boolean) - Future pageDown({required bool bottom}) async { - Map args = {}; - args.putIfAbsent("bottom", () => bottom); - return await _controller._channel.invokeMethod('pageDown', args); - } - - ///Scrolls the contents of this WebView up by half the view size. - ///Returns `true` if the page was scrolled. - /// - ///[bottom] `true` to jump to the top of the page. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#pageUp(boolean) - Future pageUp({required bool top}) async { - Map args = {}; - args.putIfAbsent("top", () => top); - return await _controller._channel.invokeMethod('pageUp', args); - } - - ///Performs zoom in in this WebView. - ///Returns `true` if zoom in succeeds, `false` if no zoom changes. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#zoomIn() - Future zoomIn() async { - Map args = {}; - return await _controller._channel.invokeMethod('zoomIn', args); - } - - ///Performs zoom out in this WebView. - ///Returns `true` if zoom out succeeds, `false` if no zoom changes. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#zoomOut() - Future zoomOut() async { - Map args = {}; - return await _controller._channel.invokeMethod('zoomOut', args); - } - - ///Clears the internal back/forward list. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearHistory() - Future clearHistory() async { - Map args = {}; - return await _controller._channel.invokeMethod('clearHistory', args); - } - - ///Clears the client certificate preferences stored in response to proceeding/cancelling client cert requests. - ///Note that WebView automatically clears these preferences when the system keychain is updated. - ///The preferences are shared by all the WebViews that are created by the embedder application. - /// - ///**NOTE**: On iOS certificate-based credentials are never stored permanently. - /// - ///**NOTE**: available on Android 21+. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#clearClientCertPreferences(java.lang.Runnable) - static Future clearClientCertPreferences() async { - Map args = {}; - await InAppWebViewController._staticChannel - .invokeMethod('clearClientCertPreferences', args); - } - - ///Returns a URL pointing to the privacy policy for Safe Browsing reporting. This value will never be `null`. - /// - ///**NOTE**: available only on Android 27+. - /// - ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl() - static Future getSafeBrowsingPrivacyPolicyUrl() async { - Map args = {}; - return await InAppWebViewController._staticChannel - .invokeMethod('getSafeBrowsingPrivacyPolicyUrl', args); - } - - ///Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks. The list is global for all the WebViews. - /// - /// Each rule should take one of these: - ///| Rule | Example | Matches Subdomain | - ///| -- | -- | -- | - ///| HOSTNAME | example.com | Yes | - ///| .HOSTNAME | .example.com | No | - ///| IPV4_LITERAL | 192.168.1.1 | No | - ///| IPV6_LITERAL_WITH_BRACKETS | [10:20:30:40:50:60:70:80] | No | - /// - ///All other rules, including wildcards, are invalid. The correct syntax for hosts is defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). - /// - ///[hosts] represents the list of hosts. This value must never be `null`. - /// - ///**NOTE**: available only on Android 27+. - /// - ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl() - static Future setSafeBrowsingWhitelist( - {required List hosts}) async { - Map args = {}; - args.putIfAbsent('hosts', () => hosts); - return await InAppWebViewController._staticChannel - .invokeMethod('setSafeBrowsingWhitelist', args); - } - - ///If WebView has already been loaded into the current process this method will return the package that was used to load it. - ///Otherwise, the package that would be used if the WebView was loaded right now will be returned; - ///this does not cause WebView to be loaded, so this information may become outdated at any time. - ///The WebView package changes either when the current WebView package is updated, disabled, or uninstalled. - ///It can also be changed through a Developer Setting. If the WebView package changes, any app process that - ///has loaded WebView will be killed. - ///The next time the app starts and loads WebView it will use the new WebView package instead. - /// - ///**NOTE**: available only on Android 26+. - /// - ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getCurrentWebViewPackage(android.content.Context) - static Future getCurrentWebViewPackage() async { - Map args = {}; - Map? packageInfo = (await InAppWebViewController - ._staticChannel - .invokeMethod('getCurrentWebViewPackage', args)) - ?.cast(); - return AndroidWebViewPackageInfo.fromMap(packageInfo); - } - - ///Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. - ///This flag can be enabled in order to facilitate debugging of web layouts and JavaScript code running inside WebViews. - ///Please refer to WebView documentation for the debugging guide. The default is `false`. - /// - ///[debuggingEnabled] whether to enable web contents debugging. - /// - ///**NOTE**: available only on Android 19+. - /// - ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#setWebContentsDebuggingEnabled(boolean) - static Future setWebContentsDebuggingEnabled(bool debuggingEnabled) async { - Map args = {}; - args.putIfAbsent('debuggingEnabled', () => debuggingEnabled); - return await InAppWebViewController._staticChannel - .invokeMethod('setWebContentsDebuggingEnabled', args); - } -} - -///Class represents the iOS controller that contains only iOS-specific methods for the WebView. -class IOSInAppWebViewController { - late InAppWebViewController _controller; - - IOSInAppWebViewController(InAppWebViewController controller) { - this._controller = controller; - } - - ///Reloads the current page, performing end-to-end revalidation using cache-validating conditionals if possible. - /// - ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/1414956-reloadfromorigin - Future reloadFromOrigin() async { - Map args = {}; - await _controller._channel.invokeMethod('reloadFromOrigin', args); - } - - ///A Boolean value indicating whether all resources on the page have been loaded over securely encrypted connections. - /// - ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/1415002-hasonlysecurecontent - Future hasOnlySecureContent() async { - Map args = {}; - return await _controller._channel - .invokeMethod('hasOnlySecureContent', args); - } - - ///Generates PDF data from the web view’s contents asynchronously. - ///Returns `null` if a problem occurred. - /// - ///[iosWKPdfConfiguration] represents the object that specifies the portion of the web view to capture as PDF data. - /// - ///**NOTE**: available only on iOS 14.0+. - /// - ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf - Future createPdf({IOSWKPDFConfiguration? iosWKPdfConfiguration}) async { - Map args = {}; - args.putIfAbsent('iosWKPdfConfiguration', () => iosWKPdfConfiguration?.toMap()); - return await _controller._channel.invokeMethod('createPdf', args); - } - - ///Creates a web archive of the web view’s current contents asynchronously. - ///Returns `null` if a problem occurred. - /// - ///**NOTE**: available only on iOS 14.0+. - /// - ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3650491-createwebarchivedata - Future createWebArchiveData() async { - Map args = {}; - return await _controller._channel.invokeMethod('createWebArchiveData', args); - } - - ///Returns a Boolean value that indicates whether WebKit natively supports resources with the specified URL scheme. - /// - ///[urlScheme] represents the URL scheme associated with the resource. - /// - ///**NOTE**: available only on iOS 11.0+. - /// - ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/2875370-handlesurlscheme - static Future handlesURLScheme(String urlScheme) async { - Map args = {}; - args.putIfAbsent('urlScheme', () => urlScheme); - return await InAppWebViewController._staticChannel - .invokeMethod('handlesURLScheme', args); - } -} +} \ No newline at end of file diff --git a/lib/src/in_app_webview/in_app_webview_options.dart b/lib/src/in_app_webview/in_app_webview_options.dart new file mode 100755 index 00000000..5f040484 --- /dev/null +++ b/lib/src/in_app_webview/in_app_webview_options.dart @@ -0,0 +1,334 @@ +import 'package:flutter/foundation.dart'; + +import 'android/in_app_webview_options.dart'; +import 'ios/in_app_webview_options.dart'; +import '../content_blocker.dart'; +import '../types.dart'; +import '../in_app_browser/in_app_browser_options.dart'; +import 'webview.dart'; + +class WebViewOptions { + Map toMap() { + return {}; + } + + static WebViewOptions fromMap(Map map) { + return new WebViewOptions(); + } + + WebViewOptions copy() { + return WebViewOptions.fromMap(this.toMap()); + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Class that represents the options that can be used for a [WebView]. +class InAppWebViewGroupOptions { + ///Cross-platform options. + late InAppWebViewOptions crossPlatform; + + ///Android-specific options. + late AndroidInAppWebViewOptions android; + + ///iOS-specific options. + late IOSInAppWebViewOptions ios; + + InAppWebViewGroupOptions({InAppWebViewOptions? crossPlatform, AndroidInAppWebViewOptions? android, IOSInAppWebViewOptions? ios}) { + this.crossPlatform = crossPlatform ?? InAppWebViewOptions(); + this.android = android ?? AndroidInAppWebViewOptions(); + this.ios = ios ?? IOSInAppWebViewOptions(); + } + + Map toMap() { + Map options = {}; + options.addAll(this.crossPlatform.toMap()); + if (defaultTargetPlatform == TargetPlatform.android) + options.addAll(this.android.toMap()); + else if (defaultTargetPlatform == TargetPlatform.iOS) options.addAll(this.ios.toMap()); + + return options; + } + + static InAppWebViewGroupOptions fromMap(Map options) { + InAppWebViewGroupOptions inAppWebViewGroupOptions = + InAppWebViewGroupOptions(); + + inAppWebViewGroupOptions.crossPlatform = + InAppWebViewOptions.fromMap(options); + if (defaultTargetPlatform == TargetPlatform.android) + inAppWebViewGroupOptions.android = + AndroidInAppWebViewOptions.fromMap(options); + else if (defaultTargetPlatform == TargetPlatform.iOS) + inAppWebViewGroupOptions.ios = IOSInAppWebViewOptions.fromMap(options); + + return inAppWebViewGroupOptions; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + InAppWebViewGroupOptions copy() { + return InAppWebViewGroupOptions.fromMap(this.toMap()); + } +} + +///This class represents all the cross-platform WebView options available. +class InAppWebViewOptions + implements WebViewOptions, BrowserOptions, AndroidOptions, IosOptions { + ///Set to `true` to be able to listen at the [WebView.shouldOverrideUrlLoading] event. The default value is `false`. + bool useShouldOverrideUrlLoading; + + ///Set to `true` to be able to listen at the [WebView.onLoadResource] event. The default value is `false`. + bool useOnLoadResource; + + ///Set to `true` to be able to listen at the [WebView.onDownloadStart] event. The default value is `false`. + bool useOnDownloadStart; + + ///Set to `true` to have all the browser's cache cleared before the new WebView is opened. The default value is `false`. + bool clearCache; + + ///Sets the user-agent for the WebView. + /// + ///**NOTE**: available on iOS 9.0+. + String userAgent; + + ///Append to the existing user-agent. Setting userAgent will override this. + /// + ///**NOTE**: available on Android 17+ and on iOS 9.0+. + String applicationNameForUserAgent; + + ///Set to `true` to enable JavaScript. The default value is `true`. + bool javaScriptEnabled; + + ///Set to `true` to allow JavaScript open windows without user interaction. The default value is `false`. + bool javaScriptCanOpenWindowsAutomatically; + + ///Set to `true` to prevent HTML5 audio or video from autoplaying. The default value is `true`. + /// + ///**NOTE**: available on iOS 10.0+. + bool mediaPlaybackRequiresUserGesture; + + ///Sets the minimum font size. The default value is `8` for Android, `0` for iOS. + int? minimumFontSize; + + ///Define whether the vertical scrollbar should be drawn or not. The default value is `true`. + bool verticalScrollBarEnabled; + + ///Define whether the horizontal scrollbar should be drawn or not. The default value is `true`. + bool horizontalScrollBarEnabled; + + ///List of custom schemes that the WebView must handle. Use the [WebView.onLoadResourceCustomScheme] event to intercept resource requests with custom scheme. + /// + ///**NOTE**: available on iOS 11.0+. + List resourceCustomSchemes; + + ///List of [ContentBlocker] that are a set of rules used to block content in the browser window. + /// + ///**NOTE**: available on iOS 11.0+. + List contentBlockers; + + ///Sets the content mode that the WebView needs to use when loading and rendering a webpage. The default value is [UserPreferredContentMode.RECOMMENDED]. + /// + ///**NOTE**: available on iOS 13.0+. + UserPreferredContentMode? preferredContentMode; + + ///Set to `true` to be able to listen at the [WebView.shouldInterceptAjaxRequest] event. The default value is `false`. + bool useShouldInterceptAjaxRequest; + + ///Set to `true` to be able to listen at the [WebView.shouldInterceptFetchRequest] event. The default value is `false`. + bool useShouldInterceptFetchRequest; + + ///Set to `true` to open a browser window with incognito mode. The default value is `false`. + /// + ///**NOTE**: available on iOS 9.0+. + ///On Android, by setting this option to `true`, it will clear all the cookies of all WebView instances, + ///because there isn't any way to make the website data store non-persistent for the specific WebView instance such as on iOS. + bool incognito; + + ///Sets whether WebView should use browser caching. The default value is `true`. + /// + ///**NOTE**: available on iOS 9.0+. + bool cacheEnabled; + + ///Set to `true` to make the background of the WebView transparent. If your app has a dark theme, this can prevent a white flash on initialization. The default value is `false`. + bool transparentBackground; + + ///Set to `true` to disable vertical scroll. The default value is `false`. + bool disableVerticalScroll; + + ///Set to `true` to disable horizontal scroll. The default value is `false`. + bool disableHorizontalScroll; + + ///Set to `true` to disable context menu. The default value is `false`. + bool disableContextMenu; + + ///Set to `false` if the WebView should not support zooming using its on-screen zoom controls and gestures. The default value is `true`. + bool supportZoom; + + ///Sets whether cross-origin requests in the context of a file scheme URL should be allowed to access content from other file scheme URLs. + ///Note that some accesses such as image HTML elements don't follow same-origin rules and aren't affected by this setting. + /// + ///Don't enable this setting if you open files that may be created or altered by external sources. + ///Enabling this setting allows malicious scripts loaded in a `file://` context to access arbitrary local files including WebView cookies and app private data. + /// + ///Note that the value of this setting is ignored if the value of [allowUniversalAccessFromFileURLs] is `true`. + /// + ///The default value is `false`. + bool allowFileAccessFromFileURLs; + + ///Sets whether cross-origin requests in the context of a file scheme URL should be allowed to access content from any origin. + ///This includes access to content from other file scheme URLs or web contexts. + ///Note that some access such as image HTML elements doesn't follow same-origin rules and isn't affected by this setting. + /// + ///Don't enable this setting if you open files that may be created or altered by external sources. + ///Enabling this setting allows malicious scripts loaded in a `file://` context to launch cross-site scripting attacks, + ///either accessing arbitrary local files including WebView cookies, app private data or even credentials used on arbitrary web sites. + /// + ///The default value is `false`. + bool allowUniversalAccessFromFileURLs; + + InAppWebViewOptions( + {this.useShouldOverrideUrlLoading = false, + this.useOnLoadResource = false, + this.useOnDownloadStart = false, + this.clearCache = false, + this.userAgent = "", + this.applicationNameForUserAgent = "", + this.javaScriptEnabled = true, + this.javaScriptCanOpenWindowsAutomatically = false, + this.mediaPlaybackRequiresUserGesture = true, + this.minimumFontSize, + this.verticalScrollBarEnabled = true, + this.horizontalScrollBarEnabled = true, + this.resourceCustomSchemes = const [], + this.contentBlockers = const [], + this.preferredContentMode = UserPreferredContentMode.RECOMMENDED, + this.useShouldInterceptAjaxRequest = false, + this.useShouldInterceptFetchRequest = false, + this.incognito = false, + this.cacheEnabled = true, + this.transparentBackground = false, + this.disableVerticalScroll = false, + this.disableHorizontalScroll = false, + this.disableContextMenu = false, + this.supportZoom = true, + this.allowFileAccessFromFileURLs = false, + this.allowUniversalAccessFromFileURLs = false}) { + if (this.minimumFontSize == null) + this.minimumFontSize = defaultTargetPlatform == TargetPlatform.android ? 8 : 0; + assert(!this.resourceCustomSchemes.contains("http") && + !this.resourceCustomSchemes.contains("https")); + } + + @override + Map toMap() { + List>> contentBlockersMapList = []; + contentBlockers.forEach((contentBlocker) { + contentBlockersMapList.add(contentBlocker.toMap()); + }); + + return { + "useShouldOverrideUrlLoading": useShouldOverrideUrlLoading, + "useOnLoadResource": useOnLoadResource, + "useOnDownloadStart": useOnDownloadStart, + "clearCache": clearCache, + "userAgent": userAgent, + "applicationNameForUserAgent": applicationNameForUserAgent, + "javaScriptEnabled": javaScriptEnabled, + "javaScriptCanOpenWindowsAutomatically": + javaScriptCanOpenWindowsAutomatically, + "mediaPlaybackRequiresUserGesture": mediaPlaybackRequiresUserGesture, + "verticalScrollBarEnabled": verticalScrollBarEnabled, + "horizontalScrollBarEnabled": horizontalScrollBarEnabled, + "resourceCustomSchemes": resourceCustomSchemes, + "contentBlockers": contentBlockersMapList, + "preferredContentMode": preferredContentMode?.toValue(), + "useShouldInterceptAjaxRequest": useShouldInterceptAjaxRequest, + "useShouldInterceptFetchRequest": useShouldInterceptFetchRequest, + "incognito": incognito, + "cacheEnabled": cacheEnabled, + "transparentBackground": transparentBackground, + "disableVerticalScroll": disableVerticalScroll, + "disableHorizontalScroll": disableHorizontalScroll, + "disableContextMenu": disableContextMenu, + "supportZoom": supportZoom, + "allowFileAccessFromFileURLs": allowFileAccessFromFileURLs, + "allowUniversalAccessFromFileURLs": allowUniversalAccessFromFileURLs + }; + } + + static InAppWebViewOptions fromMap(Map map) { + List contentBlockers = []; + List? contentBlockersMapList = map["contentBlockers"]; + if (contentBlockersMapList != null) { + contentBlockersMapList.forEach((contentBlocker) { + contentBlockers.add(ContentBlocker.fromMap( + Map>.from( + Map.from(contentBlocker)))); + }); + } + + InAppWebViewOptions options = InAppWebViewOptions(); + options.useShouldOverrideUrlLoading = map["useShouldOverrideUrlLoading"]; + options.useOnLoadResource = map["useOnLoadResource"]; + options.useOnDownloadStart = map["useOnDownloadStart"]; + options.clearCache = map["clearCache"]; + options.userAgent = map["userAgent"]; + options.applicationNameForUserAgent = map["applicationNameForUserAgent"]; + options.javaScriptEnabled = map["javaScriptEnabled"]; + options.javaScriptCanOpenWindowsAutomatically = + map["javaScriptCanOpenWindowsAutomatically"]; + options.mediaPlaybackRequiresUserGesture = + map["mediaPlaybackRequiresUserGesture"]; + options.verticalScrollBarEnabled = map["verticalScrollBarEnabled"]; + options.horizontalScrollBarEnabled = map["horizontalScrollBarEnabled"]; + options.resourceCustomSchemes = + List.from(map["resourceCustomSchemes"] ?? []); + options.contentBlockers = contentBlockers; + options.preferredContentMode = + UserPreferredContentMode.fromValue(map["preferredContentMode"]); + options.useShouldInterceptAjaxRequest = + map["useShouldInterceptAjaxRequest"]; + options.useShouldInterceptFetchRequest = + map["useShouldInterceptFetchRequest"]; + options.incognito = map["incognito"]; + options.cacheEnabled = map["cacheEnabled"]; + options.transparentBackground = map["transparentBackground"]; + options.disableVerticalScroll = map["disableVerticalScroll"]; + options.disableHorizontalScroll = map["disableHorizontalScroll"]; + options.disableContextMenu = map["disableContextMenu"]; + options.supportZoom = map["supportZoom"]; + options.allowFileAccessFromFileURLs = map["allowFileAccessFromFileURLs"]; + options.allowUniversalAccessFromFileURLs = map["allowUniversalAccessFromFileURLs"]; + return options; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } + + @override + InAppWebViewOptions copy() { + return InAppWebViewOptions.fromMap(this.toMap()); + } +} diff --git a/lib/src/in_app_webview/ios/in_app_webview_controller.dart b/lib/src/in_app_webview/ios/in_app_webview_controller.dart new file mode 100644 index 00000000..3b0a2074 --- /dev/null +++ b/lib/src/in_app_webview/ios/in_app_webview_controller.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:core'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +import '../_static_channel.dart'; + +import '../../types.dart'; + +///Class represents the iOS controller that contains only iOS-specific methods for the WebView. +class IOSInAppWebViewController { + late MethodChannel _channel; + static MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + IOSInAppWebViewController({required MethodChannel channel}) { + this._channel = channel; + } + + ///Reloads the current page, performing end-to-end revalidation using cache-validating conditionals if possible. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/1414956-reloadfromorigin + Future reloadFromOrigin() async { + Map args = {}; + await _channel.invokeMethod('reloadFromOrigin', args); + } + + ///Generates PDF data from the web view’s contents asynchronously. + ///Returns `null` if a problem occurred. + /// + ///[iosWKPdfConfiguration] represents the object that specifies the portion of the web view to capture as PDF data. + /// + ///**NOTE**: available only on iOS 14.0+. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf + Future createPdf({IOSWKPDFConfiguration? iosWKPdfConfiguration}) async { + Map args = {}; + args.putIfAbsent('iosWKPdfConfiguration', () => iosWKPdfConfiguration?.toMap()); + return await _channel.invokeMethod('createPdf', args); + } + + ///Creates a web archive of the web view’s current contents asynchronously. + ///Returns `null` if a problem occurred. + /// + ///**NOTE**: available only on iOS 14.0+. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3650491-createwebarchivedata + Future createWebArchiveData() async { + Map args = {}; + return await _channel.invokeMethod('createWebArchiveData', args); + } + + ///A Boolean value indicating whether all resources on the page have been loaded over securely encrypted connections. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/1415002-hasonlysecurecontent + Future hasOnlySecureContent() async { + Map args = {}; + return await _channel + .invokeMethod('hasOnlySecureContent', args); + } + + ///Returns a Boolean value that indicates whether WebKit natively supports resources with the specified URL scheme. + /// + ///[urlScheme] represents the URL scheme associated with the resource. + /// + ///**NOTE**: available only on iOS 11.0+. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/2875370-handlesurlscheme + static Future handlesURLScheme(String urlScheme) async { + Map args = {}; + args.putIfAbsent('urlScheme', () => urlScheme); + return await _staticChannel + .invokeMethod('handlesURLScheme', args); + } +} diff --git a/lib/src/in_app_webview/ios/in_app_webview_options.dart b/lib/src/in_app_webview/ios/in_app_webview_options.dart new file mode 100755 index 00000000..76592813 --- /dev/null +++ b/lib/src/in_app_webview/ios/in_app_webview_options.dart @@ -0,0 +1,369 @@ +import '../../types.dart'; + +import '../../in_app_browser/in_app_browser_options.dart'; + +import '../in_app_webview_options.dart'; +import '../webview.dart'; + +class IosOptions {} + +///This class represents all the iOS-only WebView options available. +class IOSInAppWebViewOptions + implements WebViewOptions, BrowserOptions, IosOptions { + ///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`. + bool disallowOverScroll; + + ///Set to `true` to allow a viewport meta tag to either disable or restrict the range of user scaling. The default value is `false`. + bool enableViewportScale; + + ///Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory. The default value is `false`. + bool suppressesIncrementalRendering; + + ///Set to `true` to allow AirPlay. The default value is `true`. + bool allowsAirPlayForMediaPlayback; + + ///Set to `true` to allow the horizontal swipe gestures trigger back-forward list navigations. The default value is `true`. + bool allowsBackForwardNavigationGestures; + + ///Set to `true` to allow that pressing on a link displays a preview of the destination for the link. The default value is `true`. + /// + ///**NOTE**: available on iOS 9.0+. + bool allowsLinkPreview; + + ///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`. + bool ignoresViewportScaleLimits; + + ///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 `