diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 24408d2b..924b998e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -16,13 +16,19 @@ <component name="ChangeListManager"> <list default="true" id="9b41f7a2-a71e-4923-91fb-249d7815b3e7" name="Default" comment=""> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java" beforeDir="false" afterPath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java" beforeDir="false" afterPath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java" beforeDir="false" afterPath="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/example/lib/main.dart" beforeDir="false" afterPath="$PROJECT_DIR$/example/lib/main.dart" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/ios/Classes/InAppBrowserOptions.swift" beforeDir="false" afterPath="$PROJECT_DIR$/ios/Classes/InAppBrowserOptions.swift" afterDir="false" /> <change beforePath="$PROJECT_DIR$/ios/Classes/InAppBrowserWebViewController.swift" beforeDir="false" afterPath="$PROJECT_DIR$/ios/Classes/InAppBrowserWebViewController.swift" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/ios/Classes/MyURLProtocol.swift" beforeDir="false" afterPath="$PROJECT_DIR$/ios/Classes/MyURLProtocol.swift" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/ios/Classes/MyURLProtocol.swift" beforeDir="false" /> + <change beforePath="$PROJECT_DIR$/ios/Classes/NSURLProtocol+WKWebVIew.h" beforeDir="false" /> + <change beforePath="$PROJECT_DIR$/ios/Classes/NSURLProtocol+WKWebVIew.m" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/ios/Classes/SwiftFlutterPlugin.swift" beforeDir="false" afterPath="$PROJECT_DIR$/ios/Classes/SwiftFlutterPlugin.swift" afterDir="false" /> <change beforePath="$PROJECT_DIR$/lib/flutter_inappbrowser.dart" beforeDir="false" afterPath="$PROJECT_DIR$/lib/flutter_inappbrowser.dart" afterDir="false" /> - <change beforePath="$PROJECT_DIR$/pubspec.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pubspec.yaml" afterDir="false" /> </list> <ignored path="$PROJECT_DIR$/.dart_tool/" /> <ignored path="$PROJECT_DIR$/.idea/" /> @@ -42,8 +48,8 @@ <file leaf-file-name="flutter_inappbrowser.dart" pinned="false" current-in-tab="false"> <entry file="file://$PROJECT_DIR$/lib/flutter_inappbrowser.dart"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="315"> - <caret line="246" column="71" selection-start-line="246" selection-start-column="71" selection-end-line="246" selection-end-column="71" /> + <state relative-caret-position="-386"> + <caret line="366" column="187" selection-start-line="366" selection-start-column="187" selection-end-line="366" selection-end-column="187" /> <folding> <element signature="e#814#834#0" expanded="true" /> </folding> @@ -54,18 +60,18 @@ <file leaf-file-name="build.gradle" pinned="false" current-in-tab="false"> <entry file="file://$PROJECT_DIR$/android/build.gradle"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="262"> + <state relative-caret-position="462"> <caret line="34" column="5" selection-start-line="34" selection-start-column="5" selection-end-line="34" selection-end-column="5" /> </state> </provider> </entry> </file> - <file leaf-file-name="README.md" pinned="false" current-in-tab="false"> + <file leaf-file-name="README.md" pinned="false" current-in-tab="true"> <entry file="file://$PROJECT_DIR$/README.md"> <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> <state split_layout="SPLIT"> - <first_editor relative-caret-position="2820"> - <caret line="188" column="158" selection-start-line="188" selection-start-column="158" selection-end-line="188" selection-end-column="158" /> + <first_editor relative-caret-position="280"> + <caret line="592" lean-forward="true" selection-start-line="592" selection-end-line="592" /> </first_editor> <second_editor> <markdownNavigatorState /> @@ -74,11 +80,11 @@ </provider> </entry> </file> - <file leaf-file-name="pubspec.yaml" pinned="false" current-in-tab="false"> - <entry file="file://$PROJECT_DIR$/pubspec.yaml"> + <file leaf-file-name="main.dart" pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/example/lib/main.dart"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="30"> - <caret line="2" column="14" selection-start-line="2" selection-start-column="14" selection-end-line="2" selection-end-column="14" /> + <state relative-caret-position="-10"> + <caret line="99" lean-forward="true" selection-start-line="99" selection-end-line="99" /> </state> </provider> </entry> @@ -87,7 +93,9 @@ <entry file="file://$PROJECT_DIR$/CHANGELOG.md"> <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> <state split_layout="SPLIT"> - <first_editor /> + <first_editor relative-caret-position="30"> + <caret line="2" column="3" selection-start-line="2" selection-start-column="3" selection-end-line="2" selection-end-column="3" /> + </first_editor> <second_editor> <markdownNavigatorState /> </second_editor> @@ -95,15 +103,6 @@ </provider> </entry> </file> - <file leaf-file-name="main.dart" pinned="false" current-in-tab="true"> - <entry file="file://$PROJECT_DIR$/example/lib/main.dart"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="537"> - <caret line="140" column="44" selection-start-line="140" selection-start-column="44" selection-end-line="140" selection-end-column="64" /> - </state> - </provider> - </entry> - </file> </leaf> </component> <component name="FileTemplateManagerImpl"> @@ -180,19 +179,18 @@ <option value="$PROJECT_DIR$/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowser.java" /> <option value="$PROJECT_DIR$/ios/flutter_inappbrowser.podspec" /> <option value="$PROJECT_DIR$/example/ios/Podfile" /> - <option value="$PROJECT_DIR$/CHANGELOG.md" /> - <option value="$PROJECT_DIR$/README.md" /> <option value="$PROJECT_DIR$/android/build.gradle" /> - <option value="$PROJECT_DIR$/lib/flutter_inappbrowser.dart" /> <option value="$PROJECT_DIR$/pubspec.yaml" /> <option value="$PROJECT_DIR$/example/lib/main.dart" /> + <option value="$PROJECT_DIR$/CHANGELOG.md" /> + <option value="$PROJECT_DIR$/lib/flutter_inappbrowser.dart" /> + <option value="$PROJECT_DIR$/README.md" /> </list> </option> </component> - <component name="ProjectFrameBounds"> - <option name="x" value="235" /> + <component name="ProjectFrameBounds" extendedState="6"> <option name="y" value="23" /> - <option name="width" value="1589" /> + <option name="width" value="1582" /> <option name="height" value="1027" /> </component> <component name="ProjectLevelVcsManager" settingsEditedManually="true" /> @@ -396,7 +394,7 @@ <servers /> </component> <component name="ToolWindowManager"> - <frame x="235" y="23" width="1589" height="1027" extended-state="0" /> + <frame x="0" y="23" width="1920" height="1057" extended-state="6" /> <editor active="true" /> <layout> <window_info anchor="bottom" id="Android Profiler" order="7" show_stripe_button="false" /> @@ -408,15 +406,15 @@ <window_info anchor="right" id="Capture Analysis" order="3" /> <window_info anchor="bottom" id="Event Log" order="8" sideWeight="0.5035553" side_tool="true" weight="0.25689086" /> <window_info anchor="bottom" id="Dart Analysis" order="14" weight="0.32855567" /> - <window_info anchor="bottom" id="Run" order="2" sideWeight="0.49644473" weight="0.39801544" /> + <window_info active="true" anchor="bottom" id="Run" order="2" sideWeight="0.49644473" visible="true" weight="0.20384204" /> <window_info anchor="bottom" id="Version Control" order="9" /> - <window_info active="true" anchor="bottom" id="Terminal" order="10" sideWeight="0.49644473" visible="true" weight="0.25689086" /> + <window_info anchor="bottom" id="Terminal" order="10" sideWeight="0.49644473" weight="0.25689086" /> <window_info anchor="right" id="Flutter Outline" order="3" weight="0.32922077" /> <window_info anchor="bottom" id="Logcat" order="11" /> <window_info id="Captures" order="2" weight="0.32936507" /> <window_info id="Capture Tool" order="2" /> <window_info id="Designer" order="2" /> - <window_info content_ui="combo" id="Project" order="0" sideWeight="0.49855492" visible="true" weight="0.18293472" /> + <window_info content_ui="combo" id="Project" order="0" sideWeight="0.49855492" visible="true" weight="0.15069222" /> <window_info id="Structure" order="1" sideWeight="0.5014451" side_tool="true" weight="0.18293472" /> <window_info anchor="right" id="Device File Explorer" order="3" side_tool="true" /> <window_info anchor="right" id="Theme Preview" order="3" /> @@ -672,38 +670,6 @@ </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/README.md"> - <provider editor-type-id="text-editor"> - <state relative-caret-position="247"> - <caret line="19" column="15" selection-start-line="19" selection-start-column="3" selection-end-line="19" selection-end-column="15" /> - </state> - </provider> - <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> - <state split_layout="SPLIT"> - <first_editor relative-caret-position="2820"> - <caret line="188" column="158" selection-start-line="188" selection-start-column="158" selection-end-line="188" selection-end-column="158" /> - </first_editor> - <second_editor> - <markdownNavigatorState /> - </second_editor> - </state> - </provider> - </entry> - <entry file="file://$PROJECT_DIR$/CHANGELOG.md"> - <provider editor-type-id="text-editor"> - <state relative-caret-position="30"> - <caret line="2" column="69" lean-forward="true" selection-start-line="2" selection-start-column="2" selection-end-line="2" selection-end-column="73" /> - </state> - </provider> - <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> - <state split_layout="SPLIT"> - <first_editor /> - <second_editor> - <markdownNavigatorState /> - </second_editor> - </state> - </provider> - </entry> <entry file="file://$PROJECT_DIR$/android/settings.gradle"> <provider selected="true" editor-type-id="text-editor"> <state> @@ -728,13 +694,6 @@ </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/android/build.gradle"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="262"> - <caret line="34" column="5" selection-start-line="34" selection-start-column="5" selection-end-line="34" selection-end-column="5" /> - </state> - </provider> - </entry> <entry file="file://$PROJECT_DIR$/pubspec.yaml"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="30"> @@ -742,20 +701,61 @@ </state> </provider> </entry> + <entry file="file://$PROJECT_DIR$/example/lib/main.dart"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-10"> + <caret line="99" lean-forward="true" selection-start-line="99" selection-end-line="99" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/CHANGELOG.md"> + <provider editor-type-id="text-editor"> + <state relative-caret-position="30"> + <caret line="2" column="69" lean-forward="true" selection-start-line="2" selection-start-column="2" selection-end-line="2" selection-end-column="73" /> + </state> + </provider> + <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> + <state split_layout="SPLIT"> + <first_editor relative-caret-position="30"> + <caret line="2" column="3" selection-start-line="2" selection-start-column="3" selection-end-line="2" selection-end-column="3" /> + </first_editor> + <second_editor> + <markdownNavigatorState /> + </second_editor> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/android/build.gradle"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="462"> + <caret line="34" column="5" selection-start-line="34" selection-start-column="5" selection-end-line="34" selection-end-column="5" /> + </state> + </provider> + </entry> <entry file="file://$PROJECT_DIR$/lib/flutter_inappbrowser.dart"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="315"> - <caret line="246" column="71" selection-start-line="246" selection-start-column="71" selection-end-line="246" selection-end-column="71" /> + <state relative-caret-position="-386"> + <caret line="366" column="187" selection-start-line="366" selection-start-column="187" selection-end-line="366" selection-end-column="187" /> <folding> <element signature="e#814#834#0" expanded="true" /> </folding> </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/example/lib/main.dart"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="537"> - <caret line="140" column="44" selection-start-line="140" selection-start-column="44" selection-end-line="140" selection-end-column="64" /> + <entry file="file://$PROJECT_DIR$/README.md"> + <provider editor-type-id="text-editor"> + <state relative-caret-position="247"> + <caret line="19" column="15" selection-start-line="19" selection-start-column="3" selection-end-line="19" selection-end-column="15" /> + </state> + </provider> + <provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]"> + <state split_layout="SPLIT"> + <first_editor relative-caret-position="280"> + <caret line="592" lean-forward="true" selection-start-line="592" selection-end-line="592" /> + </first_editor> + <second_editor> + <markdownNavigatorState /> + </second_editor> </state> </provider> </entry> diff --git a/CHANGELOG.md b/CHANGELOG.md index db90f5b4..3189cc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ +## 0.3.0 + +- fixed WebView.storyboard to deployment target 8.0 +- added `InAppBrowser.onLoadResource()` method. The event fires when the InAppBrowser webview loads a resource +- added `InAppBrowser.addJavaScriptHandler()` and `InAppBrowser.removeJavaScriptHandler()` methods to add/remove javascript message handlers +- removed `keyboardDisplayRequiresUserAction` from iOS available options +- now the `url` parameter of `InAppBrowser.open()` is optional. The default value is `about:blank` + ## 0.2.1 -- added InAppBrowser.onConsoleMessage() method to manage console messages -- fixed InAppBrowser.injectScriptCode() method when there is not a return value +- added `InAppBrowser.onConsoleMessage()` method to manage console messages +- fixed `InAppBrowser.injectScriptCode()` method when there is not a return value ## 0.2.0 diff --git a/README.md b/README.md index 94b1e01b..0023c24e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ class MyInAppBrowser extends InAppBrowser { @override Future onLoadStop(String url) async { print("\n\nStopped $url\n\n"); + + // call a javascript message handler + await this.injectScriptCode("window.flutter_inappbrowser.callHandler('handlerNameTest', 1, 5,'string', {'key': 5}, [4,6,8]);"); + // print body html print(await this.injectScriptCode("document.body.innerHTML")); @@ -78,6 +82,11 @@ class MyInAppBrowser extends InAppBrowser { this.loadUrl(url); } + @override + void onLoadResource(WebResourceResponse response, WebResourceRequest request) { + print("Started at: " + response.startTime.toString() + "ms ---> duration: " + response.duration.toString() + "ms " + response.url); + } + @override void onConsoleMessage(ConsoleMessage consoleMessage) { print(""" @@ -105,6 +114,12 @@ class _MyAppState extends State<MyApp> { @override void initState() { super.initState(); + + // listen for post messages coming from the JavaScript side + int indexTest = inAppBrowserFallback.addJavaScriptHandler("handlerNameTest", (arguments) async { + print("handlerNameTest arguments"); + print(arguments); // it prints: [1, 5, string, {key: 5}, [4, 6, 8]] + }); } @override @@ -117,7 +132,8 @@ class _MyAppState extends State<MyApp> { body: new Center( child: new RaisedButton(onPressed: () { inAppBrowser.open("https://flutter.io/", options: { - "useShouldOverrideUrlLoading": true + "useShouldOverrideUrlLoading": true, + "useOnLoadResource": true }); }, child: Text("Open InAppBrowser") @@ -134,12 +150,12 @@ class _MyAppState extends State<MyApp> { Opens a URL in a new InAppBrowser instance or the system browser. ```dart -inAppBrowser.open(String url, {Map<String, String> headers = const {}, String target = "_self", Map<String, dynamic> options = const {}}); +inAppBrowser.open({String url = "about:blank", Map<String, String> headers = const {}, String target = "_self", Map<String, dynamic> options = const {}}); ``` Opens an `url` in a new `InAppBrowser` instance or the system browser. -- `url`: The `url` to load. Call `encodeUriComponent()` on this if the `url` contains Unicode characters. +- `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. @@ -153,6 +169,7 @@ Opens an `url` in a new `InAppBrowser` instance or the system browser. All platforms support: - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the `shouldOverrideUrlLoading` event. The default value is `false`. + - __useOnLoadResource__: Set to `true` to be able to listen at the `onLoadResource()` event. The default value is `false`. - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`. - __userAgent___: Set the custom WebView's user-agent. - __javaScriptEnabled__: Set to `true` to enable JavaScript. The default value is `true`. @@ -200,6 +217,7 @@ Example: ```dart inAppBrowser.open('https://flutter.io/', options: { "useShouldOverrideUrlLoading": true, + "useOnLoadResource": true, "clearCache": true, "disallowOverScroll": true, "domStorageEnabled": true, @@ -252,7 +270,8 @@ Event fires when the `InAppBrowser` webview receives a `ConsoleMessage`. ``` Give the host application a chance to take control when a URL is about to be loaded in the current WebView. -In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`. + +**NOTE**: In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`. ```dart @override void shouldOverrideUrlLoading(String url) { @@ -260,6 +279,18 @@ In order to be able to listen this event, you need to set `useShouldOverrideUrlL } ``` +Event fires when the `InAppBrowser` webview loads a resource. + +**NOTE**: In order to be able to listen this event, you need to set `useOnLoadResource` option to `true`. + +**NOTE only for iOS**: In some cases, the `response.data` of a `response` with `text/html` encoding could be empty. +```dart + @override + void onLoadResource(WebResourceResponse response, WebResourceRequest request) { + + } +``` + #### Future\<void\> InAppBrowser.loadUrl Loads the given `url` with optional `headers` specified as a map from name to value. @@ -370,7 +401,31 @@ Injects a CSS file into the `InAppBrowser` window. (Only available when the targ ```dart inAppBrowser.injectStyleFile(String urlFile); -``` +``` + +#### int InAppBrowser.addJavaScriptHandler + +Adds/Appends a JavaScript message handler `callback` (`JavaScriptHandlerCallback`) that listen to post messages sent from JavaScript by the handler with name `handlerName`. +Returns the position `index` of the handler that can be used to remove it with the `removeJavaScriptHandler()` method. + +The Android implementation uses [addJavascriptInterface](https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface(java.lang.Object,%20java.lang.String)). +The iOS implementation uses [addScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537172-addscriptmessagehandler?language=objc) + +The JavaScript function that can be used to call the handler is `window.flutter_inappbrowser.callHandler(handlerName <String>, ...args);`, where `args` are [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters). +The `args` will be stringified automatically using `JSON.stringify(args)` method and then they will be decoded on the Dart side. + +```dart +inAppBrowser.addJavaScriptHandler(String handlerName, JavaScriptHandlerCallback callback); +``` + +#### bool InAppBrowser.removeJavaScriptHandler + +Removes a JavaScript message handler previously added with the `addJavaScriptHandler()` method in the `handlerName` list by its position `index`. +Returns `true` if the callback is removed, otherwise `false`. +```dart +inAppBrowser.removeJavaScriptHandler(String handlerName, int index); +``` + ### `ChromeSafariBrowser` class Create a Class that extends the `ChromeSafariBrowser` Class in order to override the callbacks to manage the browser events. Example: diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java index 13cd99f8..08101acb 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserOptions.java @@ -5,6 +5,7 @@ public class InAppBrowserOptions extends Options { final static String LOG_TAG = "InAppBrowserOptions"; public boolean useShouldOverrideUrlLoading = false; + public boolean useOnLoadResource = false; public boolean clearCache = false; public String userAgent = ""; public boolean javaScriptEnabled = true; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java index 557a6ce8..1e4e44e9 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserWebViewClient.java @@ -31,6 +31,7 @@ public class InAppBrowserWebViewClient extends WebViewClient { protected static final String LOG_TAG = "IABWebViewClient"; private WebViewActivity activity; Map<Integer, String> statusCodeMapping = new HashMap<Integer, String>(); + long startPageTime = 0; public InAppBrowserWebViewClient(WebViewActivity activity) { super(); @@ -164,6 +165,8 @@ public class InAppBrowserWebViewClient extends WebViewClient { public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); + startPageTime = System.currentTimeMillis(); + activity.isLoading = true; if (activity.searchView != null && !url.equals(activity.searchView.getQuery().toString())) { @@ -195,7 +198,8 @@ public class InAppBrowserWebViewClient extends WebViewClient { view.requestFocus(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - view.evaluateJavascript(activity.jsConsoleLogScript, null); + view.evaluateJavascript(WebViewActivity.consoleLogJS, null); + view.evaluateJavascript(JavaScriptBridgeInterface.flutterInAppBroserJSClass, null); } Map<String, Object> obj = new HashMap<>(); @@ -264,14 +268,24 @@ public class InAppBrowserWebViewClient extends WebViewClient { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request){ + + if (!request.getMethod().toLowerCase().equals("get") || !activity.options.useOnLoadResource) { + return null; + } + final String url = request.getUrl().toString(); - Request mRequest = new Request.Builder().url(url).build(); - try { - long loadingTime = System.currentTimeMillis(); + Request mRequest = new Request.Builder().url(url).build(); + + long startResourceTime = System.currentTimeMillis(); Response response = activity.httpClient.newCall(mRequest).execute(); - loadingTime = System.currentTimeMillis() - loadingTime; + long startTime = startResourceTime - startPageTime; + long duration = System.currentTimeMillis() - startResourceTime; + + if (response.cacheResponse() != null) { + duration = 0; + } String reasonPhrase = response.message(); if (reasonPhrase.equals("")) { @@ -281,20 +295,20 @@ public class InAppBrowserWebViewClient extends WebViewClient { Map<String, String> headersResponse = new HashMap<String, String>(); for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) { - String value = ""; + StringBuilder value = new StringBuilder(); for (String val: entry.getValue()) { - value += (value == "") ? val : "; " + val; + value.append( (value.toString().isEmpty()) ? val : "; " + val ); } - headersResponse.put(entry.getKey().toLowerCase(), value); + headersResponse.put(entry.getKey().toLowerCase(), value.toString()); } Map<String, String> headersRequest = new HashMap<String, String>(); for (Map.Entry<String, List<String>> entry : mRequest.headers().toMultimap().entrySet()) { - String value = ""; + StringBuilder value = new StringBuilder(); for (String val: entry.getValue()) { - value += (value == "") ? val : "; " + val; + value.append( (value.toString().isEmpty()) ? val : "; " + val ); } - headersRequest.put(entry.getKey().toLowerCase(), value); + headersRequest.put(entry.getKey().toLowerCase(), value.toString()); } Map<String, Object> obj = new HashMap<>(); @@ -309,7 +323,8 @@ public class InAppBrowserWebViewClient extends WebViewClient { res.put("url", url); res.put("statusCode", response.code()); res.put("headers", headersResponse); - res.put("loadingTime", loadingTime); + res.put("startTime", startTime); + res.put("duration", duration); res.put("data", dataBytes); req.put("url", url); @@ -332,7 +347,11 @@ public class InAppBrowserWebViewClient extends WebViewClient { } catch (IOException e) { e.printStackTrace(); Log.d(LOG_TAG, e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + Log.d(LOG_TAG, e.getMessage()); } + return null; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/JavaScriptBridgeInterface.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/JavaScriptBridgeInterface.java new file mode 100644 index 00000000..88117897 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/JavaScriptBridgeInterface.java @@ -0,0 +1,29 @@ +package com.pichillilorenzo.flutter_inappbrowser; + +import android.webkit.JavascriptInterface; + +import java.util.HashMap; +import java.util.Map; + +public class JavaScriptBridgeInterface { + private static final String LOG_TAG = "JSBridgeInterface"; + static final String name = "flutter_inappbrowser"; + WebViewActivity activity; + + static final String flutterInAppBroserJSClass = "window." + name + ".callHandler = function(handlerName, ...args) {\n" + + "window." + name + "._callHandler(handlerName, JSON.stringify(args));\n" + + "}\n"; + + JavaScriptBridgeInterface(WebViewActivity a) { + activity = a; + } + + @JavascriptInterface + public void _callHandler(String handlerName, String args) { + Map<String, Object> obj = new HashMap<>(); + obj.put("uuid", activity.uuid); + obj.put("handlerName", handlerName); + obj.put("args", args); + InAppBrowserFlutterPlugin.channel.invokeMethod("onCallJsHandler", obj); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java index cafbaabd..97d859ad 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/WebViewActivity.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import io.flutter.plugin.common.MethodChannel; +import okhttp3.Cache; import okhttp3.OkHttpClient; public class WebViewActivity extends AppCompatActivity { @@ -40,7 +41,7 @@ public class WebViewActivity extends AppCompatActivity { public boolean isHidden = false; OkHttpClient httpClient; - static final String jsConsoleLogScript = "(function() {\n"+ + static final String consoleLogJS = "(function() {\n"+ " var oldLogs = {\n"+ " 'log': console.log,\n"+ " 'debug': console.debug,\n"+ @@ -89,14 +90,18 @@ public class WebViewActivity extends AppCompatActivity { prepareWebView(); - httpClient = new OkHttpClient(); + int cacheSize = 10 * 1024 * 1024; // 10MB + httpClient = new OkHttpClient().newBuilder().cache(new Cache(getApplicationContext().getCacheDir(), cacheSize)).build(); webView.loadUrl(url, headers); + //webView.loadData("<!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"UTF-8\"> <title>Document</title> </head> <body> ciao <img src=\"https://via.placeholder.com/350x150\" /> <img src=\"./images/test\" alt=\"not found\" /></body> </html>", "text/html", "utf8"); } private void prepareWebView() { + webView.addJavascriptInterface(new JavaScriptBridgeInterface(this), JavaScriptBridgeInterface.name); + inAppBrowserWebChromeClient = new InAppBrowserWebChromeClient(this); webView.setWebChromeClient(inAppBrowserWebChromeClient); diff --git a/example/lib/main.dart b/example/lib/main.dart index 1ae29afc..899c64e1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,8 +13,9 @@ class MyInAppBrowser extends InAppBrowser { Future onLoadStop(String url) async { print("\n\nStopped $url\n\n"); -// // javascript error -// await this.injectScriptCode("console.log({'testJavaScriptError': 5}));"); + await this.injectScriptCode("window.flutter_inappbrowser.callHandler('handlerTest', 1, 5,'string', {'key': 5}, [4,6,8]);"); + await this.injectScriptCode("window.flutter_inappbrowser.callHandler('handlerTest2', false, null, undefined);"); + await this.injectScriptCode("setTimeout(function(){window.flutter_inappbrowser.callHandler('handlerTest', 'anotherString');}, 1000);"); // // await this.injectScriptCode("console.log({'testObject': 5});"); // await this.injectScriptCode("console.warn('testWarn',null);"); @@ -73,20 +74,21 @@ class MyInAppBrowser extends InAppBrowser { @override void onLoadResource(WebResourceResponse response, WebResourceRequest request) { - print(response.loadingTime.toString() + "ms " + response.url); - if (response.headers["content-length"] != null) - print(response.headers["content-length"] + " length"); + + print("Started at: " + response.startTime.toString() + "ms ---> duration: " + response.duration.toString() + "ms " + response.url); +// if (response.headers["content-length"] != null) +// print(response.headers["content-length"] + " length"); } @override void onConsoleMessage(ConsoleMessage consoleMessage) { - print(""" - console output: - sourceURL: ${consoleMessage.sourceURL} - lineNumber: ${consoleMessage.lineNumber} - message: ${consoleMessage.message} - messageLevel: ${consoleMessage.messageLevel} - """); +// print(""" +// console output: +// sourceURL: ${consoleMessage.sourceURL} +// lineNumber: ${consoleMessage.lineNumber} +// message: ${consoleMessage.message} +// messageLevel: ${consoleMessage.messageLevel} +// """); } } @@ -112,9 +114,12 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { } } +// adding a webview fallback MyChromeSafariBrowser chromeSafariBrowser = new MyChromeSafariBrowser(inAppBrowserFallback); -void main() => runApp(new MyApp()); +void main() { + runApp(new MyApp()); +} class MyApp extends StatefulWidget { @override @@ -126,6 +131,15 @@ class _MyAppState extends State<MyApp> { @override void initState() { super.initState(); + int indexTest = inAppBrowserFallback.addJavaScriptHandler("handlerTest", (arguments) async { + print("handlerTest arguments"); + print(arguments); + }); + int indexTest2 = inAppBrowserFallback.addJavaScriptHandler("test2", (arguments) async { + print("handlerTest2 arguments"); + print(arguments); + inAppBrowserFallback.removeJavaScriptHandler("test", indexTest); + }); } @override @@ -139,6 +153,7 @@ class _MyAppState extends State<MyApp> { child: new RaisedButton(onPressed: () { //chromeSafariBrowser.open("https://flutter.io/"); inAppBrowserFallback.open(url: "https://flutter.io/", options: { + //"useOnLoadResource": true, //"hidden": true, //"toolbarTopFixedTitle": "Fixed title", //"useShouldOverrideUrlLoading": true diff --git a/ios/Classes/InAppBrowserOptions.swift b/ios/Classes/InAppBrowserOptions.swift index 252e7376..121b9360 100644 --- a/ios/Classes/InAppBrowserOptions.swift +++ b/ios/Classes/InAppBrowserOptions.swift @@ -11,6 +11,7 @@ import Foundation public class InAppBrowserOptions: Options { var useShouldOverrideUrlLoading = false + var useOnLoadResource = false var clearCache = false var userAgent = "" var javaScriptEnabled = true diff --git a/ios/Classes/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowserWebViewController.swift index e41502fa..b9bc369e 100644 --- a/ios/Classes/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowserWebViewController.swift @@ -15,7 +15,7 @@ typealias OlderClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, B typealias NewerClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void // the message needs to be concatenated with '' in order to have the same behavior like on Android -let jsConsoleLog = """ +let consoleLogJS = """ (function() { var oldLogs = { 'consoleLog': console.log, @@ -44,53 +44,91 @@ let jsConsoleLog = """ })(); """ -extension WKWebView{ - - var keyboardDisplayRequiresUserAction: Bool? { - get { - return self.keyboardDisplayRequiresUserAction - } - set { - self.setKeyboardRequiresUserInteraction(newValue ?? true) - } - } - - func setKeyboardRequiresUserInteraction( _ value: Bool) { - - guard - let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else { - print("Cannot find the WKContentView class") - return - } - - let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:") - let newerSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") - - if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) { - - let originalImp: IMP = method_getImplementation(method) - let original: OlderClosureType = unsafeBitCast(originalImp, to: OlderClosureType.self) - let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in - original(me, olderSelector, arg0, !value, arg2, arg3) - } - let imp: IMP = imp_implementationWithBlock(block) - method_setImplementation(method, imp) - } - - if let method = class_getInstanceMethod(WKContentViewClass, newerSelector) { - - let originalImp: IMP = method_getImplementation(method) - let original: NewerClosureType = unsafeBitCast(originalImp, to: NewerClosureType.self) - let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in - original(me, newerSelector, arg0, !value, arg2, arg3, arg4) - } - let imp: IMP = imp_implementationWithBlock(block) - method_setImplementation(method, imp) - } - - } - +let resourceObserverJS = """ +(function() { + var observer = new PerformanceObserver(function(list) { + list.getEntries().forEach(function(entry) { + window.webkit.messageHandlers['resourceLoaded'].postMessage(JSON.stringify(entry)); + }); + }); + observer.observe({entryTypes: ['resource', 'mark', 'measure']}); +})(); +""" + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappbrowser" + +let javaScriptBridgeJS = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function(handlerName, ...args) { + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': handlerName, 'args': JSON.stringify(args)} ); } +""" + +func currentTimeInMilliSeconds() -> Int { + let currentDate = Date() + let since1970 = currentDate.timeIntervalSince1970 + return Int(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 +} + + +//extension WKWebView{ +// +// var keyboardDisplayRequiresUserAction: Bool? { +// get { +// return self.keyboardDisplayRequiresUserAction +// } +// set { +// self.setKeyboardRequiresUserInteraction(newValue ?? true) +// } +// } +// +// func setKeyboardRequiresUserInteraction( _ value: Bool) { +// +// guard +// let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else { +// print("Cannot find the WKContentView class") +// return +// } +// +// let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:") +// let newerSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") +// +// if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) { +// +// let originalImp: IMP = method_getImplementation(method) +// let original: OlderClosureType = unsafeBitCast(originalImp, to: OlderClosureType.self) +// let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in +// original(me, olderSelector, arg0, !value, arg2, arg3) +// } +// let imp: IMP = imp_implementationWithBlock(block) +// method_setImplementation(method, imp) +// } +// +// if let method = class_getInstanceMethod(WKContentViewClass, newerSelector) { +// +// let originalImp: IMP = method_getImplementation(method) +// let original: NewerClosureType = unsafeBitCast(originalImp, to: NewerClosureType.self) +// let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in +// original(me, newerSelector, arg0, !value, arg2, arg3, arg4) +// } +// let imp: IMP = imp_implementationWithBlock(block) +// method_setImplementation(method, imp) +// } +// +// } +// +//} class WKWebView_IBWrapper: WKWebView { required convenience init?(coder: NSCoder) { @@ -100,7 +138,7 @@ class WKWebView_IBWrapper: WKWebView { } } -class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, UITextFieldDelegate, WKScriptMessageHandler, MyURLProtocolDelegate { +class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, UITextFieldDelegate, WKScriptMessageHandler { @IBOutlet var webView: WKWebView_IBWrapper! @IBOutlet var closeButton: UIButton! @IBOutlet var reloadButton: UIBarButtonItem! @@ -119,6 +157,8 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio var initHeaders: [String: String]? var isHidden = false var uuid: String = "" + var WKNavigationMap: [String: [String: Any]] = [:] + var startPageTime = 0 required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! @@ -132,7 +172,7 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio override func viewDidLoad() { super.viewDidLoad() - MyURLProtocol.wkWebViewDelegateMap[uuid] = self + //MyURLProtocol.wkWebViewDelegateMap[uuid] = self webView.uiDelegate = self webView.navigationDelegate = self @@ -162,6 +202,7 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio spinner.stopAnimating() loadUrl(url: self.currentURL!, headers: self.initHeaders) + } // Prevent crashes on closing windows @@ -249,14 +290,25 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio let jscriptWebkitTouchCallout = WKUserScript(source: "document.body.style.webkitTouchCallout='none';", injectionTime: .atDocumentEnd, forMainFrameOnly: true) self.webView.configuration.userContentController.addUserScript(jscriptWebkitTouchCallout) - let jsConsoleLogScript = WKUserScript(source: jsConsoleLog, injectionTime: .atDocumentStart, forMainFrameOnly: false) - self.webView.configuration.userContentController.addUserScript(jsConsoleLogScript) + + let consoleLogJSScript = WKUserScript(source: consoleLogJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) + self.webView.configuration.userContentController.addUserScript(consoleLogJSScript) self.webView.configuration.userContentController.add(self, name: "consoleLog") self.webView.configuration.userContentController.add(self, name: "consoleDebug") self.webView.configuration.userContentController.add(self, name: "consoleError") self.webView.configuration.userContentController.add(self, name: "consoleInfo") self.webView.configuration.userContentController.add(self, name: "consoleWarn") + let javaScriptBridgeJSScript = WKUserScript(source: javaScriptBridgeJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) + self.webView.configuration.userContentController.addUserScript(javaScriptBridgeJSScript) + self.webView.configuration.userContentController.add(self, name: "callHandler") + + if (browserOptions?.useOnLoadResource)! { + let resourceObserverJSScript = WKUserScript(source: resourceObserverJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) + self.webView.configuration.userContentController.addUserScript(resourceObserverJSScript) + self.webView.configuration.userContentController.add(self, name: "resourceLoaded") + } + if #available(iOS 10.0, *) { if (browserOptions?.mediaPlaybackRequiresUserGesture)! { self.webView.configuration.mediaTypesRequiringUserActionForPlayback = .all @@ -301,21 +353,6 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio } } - // set uuid in the User-Agent in order to know which webview is making internal requests and - // to send the onLoadResource event to the correct webview - if #available(iOS 9.0, *) { - if (self.webView.customUserAgent != nil) { - self.webView.customUserAgent = self.webView.customUserAgent! + " WKWebView/" + self.uuid - } - else { - self.webView.evaluateJavaScript("navigator.userAgent") { [weak webView] (result, error) in - if let webView = self.webView, let userAgent = result as? String { - webView.customUserAgent = userAgent + " WKWebView/" + self.uuid - } - } - } - } - if (browserOptions?.clearCache)! { clearCache() } @@ -480,18 +517,27 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - let url = navigationAction.request.url - - if (url != nil && navigationAction.navigationType == .linkActivated && (browserOptions?.useShouldOverrideUrlLoading)!) { - navigationDelegate?.shouldOverrideUrlLoading(uuid: self.uuid, webView: webView, url: url!) - decisionHandler(.cancel) - return + if let url = navigationAction.request.url { + + if url.absoluteString != self.currentURL?.absoluteString && (browserOptions?.useOnLoadResource)! { + WKNavigationMap[url.absoluteString] = [ + "startTime": currentTimeInMilliSeconds(), + "request": navigationAction.request + ] + } + + if navigationAction.navigationType == .linkActivated && (browserOptions?.useShouldOverrideUrlLoading)! { + navigationDelegate?.shouldOverrideUrlLoading(uuid: self.uuid, webView: webView, url: url) + decisionHandler(.cancel) + return + } + + if navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .backForward { + currentURL = url + updateUrlTextField(url: (url.absoluteString)) + } } - if url != nil && (navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .backForward) { - currentURL = url - updateUrlTextField(url: (url?.absoluteString)!) - } decisionHandler(.allow) } @@ -499,9 +545,18 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { - //dump((navigationResponse.response as! HTTPURLResponse)) - //print(navigationResponse.response.mimeType) - //print(navigationResponse.response.url) + + if (browserOptions?.useOnLoadResource)! { + if let url = navigationResponse.response.url { + if WKNavigationMap[url.absoluteString] != nil { + let startResourceTime = (WKNavigationMap[url.absoluteString]!["startTime"] as! Int) + let startTime = startResourceTime - startPageTime; + let duration = currentTimeInMilliSeconds() - startResourceTime; + self.didReceiveResourceResponse(navigationResponse.response, fromRequest: WKNavigationMap[url.absoluteString]!["request"] as? URLRequest, withData: Data(), startTime: startTime, duration: duration) + } + } + } + decisionHandler(.allow) } @@ -540,6 +595,9 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio // } func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + + self.startPageTime = currentTimeInMilliSeconds() + // loading url, start spinner, update back/forward backButton.isEnabled = webView.canGoBack forwardButton.isEnabled = webView.canGoForward @@ -552,8 +610,8 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.WKNavigationMap = [:] // update url, stop spinner, update back/forward - currentURL = webView.url updateUrlTextField(url: (currentURL?.absoluteString)!) backButton.isEnabled = webView.canGoBack @@ -576,8 +634,8 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio navigationDelegate?.onLoadError(uuid: self.uuid, webView: webView, error: error) } - func didReceiveResponse(_ response: URLResponse, fromRequest request: URLRequest?, withData data: Data, loadingTime time: Int) { - navigationDelegate?.onLoadResource(uuid: self.uuid, webView: webView, response: response, fromRequest: request, withData: data, loadingTime: time) + func didReceiveResourceResponse(_ response: URLResponse, fromRequest request: URLRequest?, withData data: Data, startTime: Int, duration: Int) { + navigationDelegate?.onLoadResource(uuid: self.uuid, webView: webView, response: response, fromRequest: request, withData: data, startTime: startTime, duration: duration) } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { @@ -607,5 +665,37 @@ class InAppBrowserWebViewController: UIViewController, WKUIDelegate, WKNavigatio } navigationDelegate?.onConsoleMessage(uuid: self.uuid, sourceURL: "", lineNumber: 1, message: message.body as! String, messageLevel: messageLevel) } + else if message.name == "resourceLoaded" { + if let resource = convertToDictionary(text: message.body as! String) { + let url = URL(string: resource["name"] as! String)! + if !UIApplication.shared.canOpenURL(url) { + return + } + let startTime = Int(resource["startTime"] as! Double) + let duration = Int(resource["duration"] as! Double) + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = [:] + let config = URLSessionConfiguration.default + let session = URLSession(configuration: config) + let task = session.dataTask(with: urlRequest) { (data, response, error) in + if error != nil { + print(error) + return + } + var withData = data + if withData == nil { + withData = Data() + } + self.didReceiveResourceResponse(response!, fromRequest: urlRequest, withData: withData!, startTime: startTime, duration: duration) + } + task.resume() + } + } + else if message.name == "callHandler" { + let body = message.body as! [String: Any] + let handlerName = body["handlerName"] as! String + let args = body["args"] as! String + self.navigationDelegate?.onCallJsHandler(uuid: self.uuid, webView: webView, handlerName: handlerName, args: args) + } } } diff --git a/ios/Classes/MyURLProtocol.swift b/ios/Classes/MyURLProtocol.swift deleted file mode 100644 index c08ed8fe..00000000 --- a/ios/Classes/MyURLProtocol.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// MyURLProtocol.swift -// Pods -// -// Created by Lorenzo on 12/10/18. -// - -import Foundation -import WebKit - - -func currentTimeInMilliSeconds() -> Int { - let currentDate = Date() - let since1970 = currentDate.timeIntervalSince1970 - return Int(since1970 * 1000) -} - -class MyURLProtocol: URLProtocol { - -// struct Constants { -// static let RequestHandledKey = "URLProtocolRequestHandled" -// } - - var wkWebViewUuid: String? - var session: URLSession? - var sessionTask: URLSessionDataTask? - var response: URLResponse? - var data: Data? - static var wkWebViewDelegateMap: [String: MyURLProtocolDelegate] = [:] - var loadingTime: Int = 0 - - override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { - super.init(request: request, cachedResponse: cachedResponse, client: client) - - self.wkWebViewUuid = MyURLProtocol.getUuid(request) - - if session == nil && self.wkWebViewUuid != nil { - session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) - } - } - - override class func canInit(with request: URLRequest) -> Bool { - - if getUuid(request) == nil { - return false - } -// if MyURLProtocol.property(forKey: Constants.RequestHandledKey, in: request) != nil { -// return false -// } - return true - } - - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - - override func startLoading() { - let newRequest = ((request as NSURLRequest).mutableCopy() as? NSMutableURLRequest)! - loadingTime = currentTimeInMilliSeconds() - //MyURLProtocol.setProperty(true, forKey: Constants.RequestHandledKey, in: newRequest) - sessionTask = session?.dataTask(with: newRequest as URLRequest) - sessionTask?.resume() - } - - override func stopLoading() { - if let uuid = self.wkWebViewUuid { - if MyURLProtocol.wkWebViewDelegateMap[uuid] != nil && self.response != nil { - loadingTime = currentTimeInMilliSeconds() - loadingTime - if self.data == nil { - self.data = Data() - } - MyURLProtocol.wkWebViewDelegateMap[uuid]!.didReceiveResponse(self.response!, fromRequest: request, withData: self.data!, loadingTime: loadingTime) - } - } - - sessionTask?.cancel() - } - - class func getUuid(_ request: URLRequest?) -> String? { - let userAgent: String? = request?.allHTTPHeaderFields?["User-Agent"] - var uuid: String? = nil - if userAgent != nil { - if userAgent!.contains("WKWebView/") { - let userAgentSplitted = userAgent!.split(separator: " ") - uuid = String(userAgentSplitted[userAgentSplitted.count-1]).replacingOccurrences(of: "WKWebView/", with: "") - } - } - return uuid - } -} - -extension MyURLProtocol: URLSessionDataDelegate { - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if self.data == nil { - self.data = data - } - else { - self.data!.append(data) - } - client?.urlProtocol(self, didLoad: data) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - let policy = URLCache.StoragePolicy(rawValue: request.cachePolicy.rawValue) ?? .notAllowed - self.response = response - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: policy) - completionHandler(.allow) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if let error = error { - client?.urlProtocol(self, didFailWithError: error) - } else { - client?.urlProtocolDidFinishLoading(self) - } - } - - func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { - client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response) - completionHandler(request) - } - - func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - guard let error = error else { return } - client?.urlProtocol(self, didFailWithError: error) - } - - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - let protectionSpace = challenge.protectionSpace - let sender = challenge.sender - - if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - if let serverTrust = protectionSpace.serverTrust { - let credential = URLCredential(trust: serverTrust) - sender?.use(credential, for: challenge) - completionHandler(.useCredential, credential) - return - } - } - } - - func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { - client?.urlProtocolDidFinishLoading(self) - } -} - -protocol MyURLProtocolDelegate { - func didReceiveResponse(_ response: URLResponse, fromRequest request: URLRequest?, withData data: Data, loadingTime time: Int) -} diff --git a/ios/Classes/NSURLProtocol+WKWebVIew.h b/ios/Classes/NSURLProtocol+WKWebVIew.h deleted file mode 100644 index d0dd71f3..00000000 --- a/ios/Classes/NSURLProtocol+WKWebVIew.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// NSURLProtocol+WKWebVIew.h -// Pods -// -// Created by Lorenzo on 11/10/18. -// - -#ifndef NSURLProtocol_WKWebVIew_h -#define NSURLProtocol_WKWebVIew_h - - -#endif /* NSURLProtocol_WKWebVIew_h */ - -#import <Foundation/Foundation.h> - -@interface NSURLProtocol (WKWebVIew) - -+ (void)wk_registerScheme:(NSString*)scheme; - -+ (void)wk_unregisterScheme:(NSString*)scheme; - - -@end diff --git a/ios/Classes/NSURLProtocol+WKWebVIew.m b/ios/Classes/NSURLProtocol+WKWebVIew.m deleted file mode 100644 index 176ded26..00000000 --- a/ios/Classes/NSURLProtocol+WKWebVIew.m +++ /dev/null @@ -1,54 +0,0 @@ -// -// NSURLProtocol+WKWebVIew.m -// Pods -// -// Created by Lorenzo on 11/10/18. -// - -#import <Foundation/Foundation.h> -#import "NSURLProtocol+WKWebVIew.h" -#import <WebKit/WebKit.h> -//FOUNDATION_STATIC_INLINE 属于属于runtime范畴,你的.m文件需要频繁调用一个函数,可以用static inline来声明。从SDWebImage从get到的。 -FOUNDATION_STATIC_INLINE Class ContextControllerClass() { - static Class cls; - if (!cls) { - cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class]; - } - return cls; -} - -FOUNDATION_STATIC_INLINE SEL RegisterSchemeSelector() { - return NSSelectorFromString(@"registerSchemeForCustomProtocol:"); -} - -FOUNDATION_STATIC_INLINE SEL UnregisterSchemeSelector() { - return NSSelectorFromString(@"unregisterSchemeForCustomProtocol:"); -} - -@implementation NSURLProtocol (WebKitSupport) - -+ (void)wk_registerScheme:(NSString *)scheme { - Class cls = ContextControllerClass(); - SEL sel = RegisterSchemeSelector(); - if ([(id)cls respondsToSelector:sel]) { - // 放弃编辑器警告 -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [(id)cls performSelector:sel withObject:scheme]; -#pragma clang diagnostic pop - } -} - -+ (void)wk_unregisterScheme:(NSString *)scheme { - Class cls = ContextControllerClass(); - SEL sel = UnregisterSchemeSelector(); - if ([(id)cls respondsToSelector:sel]) { - // 放弃编辑器警告 -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [(id)cls performSelector:sel withObject:scheme]; -#pragma clang diagnostic pop - } -} - -@end diff --git a/ios/Classes/SwiftFlutterPlugin.swift b/ios/Classes/SwiftFlutterPlugin.swift index 4b782de2..bcfa0ec7 100644 --- a/ios/Classes/SwiftFlutterPlugin.swift +++ b/ios/Classes/SwiftFlutterPlugin.swift @@ -22,6 +22,19 @@ import Foundation import AVFoundation import SafariServices +//class CustomURLCache: URLCache { +// override func cachedResponse(for request: URLRequest) -> CachedURLResponse? { +// dump(request.url) +// return super.cachedResponse(for: request) +// } +// +// override func getCachedResponse(for dataTask: URLSessionDataTask, +// completionHandler: @escaping (CachedURLResponse?) -> Void) { +// dump(dataTask.response) +// super.getCachedResponse(for: dataTask, completionHandler: completionHandler) +// } +//} + let WEBVIEW_STORYBOARD = "WebView" let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" @@ -46,9 +59,12 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { } public static func register(with registrar: FlutterPluginRegistrar) { - URLProtocol.wk_registerScheme("http") - URLProtocol.wk_registerScheme("https") - URLProtocol.registerClass(MyURLProtocol.self) +// URLProtocol.wk_registerScheme("http") +// URLProtocol.wk_registerScheme("https") +// URLProtocol.registerClass(MyURLProtocol.self) + + //URLCache.shared = CustomURLCache() + let channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappbrowser", binaryMessenger: registrar.messenger()) let instance = SwiftFlutterPlugin(with: registrar) registrar.addMethodCallDelegate(instance, channel: channel) @@ -423,7 +439,8 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { if error != nil { let userInfo = (error! as NSError).userInfo - self.onConsoleMessage(uuid: uuid, sourceURL: (userInfo["WKJavaScriptExceptionSourceURL"] as! URL).absoluteString, lineNumber: userInfo["WKJavaScriptExceptionLineNumber"] as! Int, message: userInfo["WKJavaScriptExceptionMessage"] as! String, messageLevel: "ERROR") + dump(userInfo) + self.onConsoleMessage(uuid: uuid, sourceURL: (userInfo["WKJavaScriptExceptionSourceURL"] as? URL)?.absoluteString ?? "", lineNumber: userInfo["WKJavaScriptExceptionLineNumber"] as! Int, message: userInfo["WKJavaScriptExceptionMessage"] as! String, messageLevel: "ERROR") } if value == nil { @@ -492,7 +509,7 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { } } - func onLoadResource(uuid: String, webView: WKWebView, response: URLResponse, fromRequest request: URLRequest?, withData data: Data, loadingTime time: Int) { + func onLoadResource(uuid: String, webView: WKWebView, response: URLResponse, fromRequest request: URLRequest?, withData data: Data, startTime: Int, duration: Int) { if self.webViewControllers[uuid] != nil { var headersResponse = (response as! HTTPURLResponse).allHeaderFields as! [String: String] headersResponse.lowercaseKeys() @@ -506,7 +523,8 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { "url": response.url!.absoluteString, "statusCode": (response as! HTTPURLResponse).statusCode, "headers": headersResponse, - "loadingTime": time, + "startTime": startTime, + "duration": duration, "data": data ], "request": [ @@ -551,6 +569,10 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin { channel.invokeMethod("onChromeSafariBrowserClosed", arguments: ["uuid": uuid]) } + func onCallJsHandler(uuid: String, webView: WKWebView, handlerName: String, args: String) { + channel.invokeMethod("onCallJsHandler", arguments: ["uuid": uuid, "handlerName": handlerName, "args": args]) + } + func safariExit(uuid: String) { if let safariViewController = self.safariViewControllers[uuid] { if #available(iOS 9.0, *) { diff --git a/lib/flutter_inappbrowser.dart b/lib/flutter_inappbrowser.dart index 8926d9b1..750f3885 100644 --- a/lib/flutter_inappbrowser.dart +++ b/lib/flutter_inappbrowser.dart @@ -22,11 +22,13 @@ import 'dart:async'; import 'dart:collection'; import 'dart:typed_data'; +import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:uuid/uuid.dart'; typedef Future<dynamic> ListenerCallback(MethodCall call); +typedef Future<void> JavaScriptHandlerCallback(List<dynamic> arguments); var _uuidGenerator = new Uuid(); @@ -35,6 +37,8 @@ enum ConsoleMessageLevel { DEBUG, ERROR, LOG, TIP, WARNING } +///Public class representing a resource request of the [InAppBrowser] WebView. +///It is used by the method [InAppBrowser.onLoadResource()]. class WebResourceRequest { String url; @@ -45,15 +49,18 @@ class WebResourceRequest { } +///Public class representing a resource response of the [InAppBrowser] WebView. +///It is used by the method [InAppBrowser.onLoadResource()]. class WebResourceResponse { String url; Map<String, String> headers; int statusCode; - int loadingTime; + int startTime; + int duration; Uint8List data; - WebResourceResponse(this.url, this.headers, this.statusCode, this.loadingTime, this.data); + WebResourceResponse(this.url, this.headers, this.statusCode, this.startTime, this.duration, this.data); } @@ -99,6 +106,7 @@ class _ChannelManager { class InAppBrowser { String uuid; + Map<String, List<JavaScriptHandlerCallback>> javaScriptHandlersMap = HashMap<String, List<JavaScriptHandlerCallback>>(); /// InAppBrowser () { @@ -139,7 +147,8 @@ class InAppBrowser { Map<dynamic, dynamic> headersResponse = rawResponse["headers"]; headersResponse = headersResponse.cast<String, String>(); int statusCode = rawResponse["statusCode"]; - int loadingTime = rawResponse["loadingTime"]; + int startTime = rawResponse["startTime"]; + int duration = rawResponse["duration"]; Uint8List data = rawResponse["data"]; String urlRequest = rawRequest["url"]; @@ -147,7 +156,7 @@ class InAppBrowser { headersRequest = headersResponse.cast<String, String>(); String method = rawRequest["method"]; - var response = new WebResourceResponse(urlResponse, headersResponse, statusCode, loadingTime, data); + var response = new WebResourceResponse(urlResponse, headersResponse, statusCode, startTime, duration, data); var request = new WebResourceRequest(urlRequest, headersRequest, method); onLoadResource(response, request); @@ -165,13 +174,22 @@ class InAppBrowser { }); onConsoleMessage(ConsoleMessage(sourceURL, lineNumber, message, messageLevel)); break; + case "onCallJsHandler": + String handlerName = call.arguments["handlerName"]; + List<dynamic> args = jsonDecode(call.arguments["args"]); + if (javaScriptHandlersMap.containsKey(handlerName)) { + for (var handler in javaScriptHandlersMap[handlerName]) { + handler(args); + } + } + break; } return new Future.value(""); } ///Opens an [url] in a new [InAppBrowser] instance or the system browser. /// - ///- [url]: The [url] to load. Call [encodeUriComponent()] on this if the [url] contains Unicode characters. + ///- [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. /// @@ -185,6 +203,7 @@ class InAppBrowser { /// /// All platforms support: /// - __useShouldOverrideUrlLoading__: Set to `true` to be able to listen at the [shouldOverrideUrlLoading()] event. The default value is `false`. + /// - __useOnLoadResource__: Set to `true` to be able to listen at the [onLoadResource()] event. The default value is `false`. /// - __clearCache__: Set to `true` to have all the browser's cache cleared before the new window is opened. The default value is `false`. /// - __userAgent___: Set the custom WebView's user-agent. /// - __javaScriptEnabled__: Set to `true` to enable JavaScript. The default value is `true`. @@ -342,6 +361,33 @@ class InAppBrowser { return await _ChannelManager.channel.invokeMethod('injectStyleFile', args); } + ///Adds/Appends a JavaScript message handler [callback] ([JavaScriptHandlerCallback]) that listen to post messages sent from JavaScript by the handler with name [handlerName]. + ///Returns the position `index` of the handler that can be used to remove it with the [removeJavaScriptHandler()] method. + /// + ///The Android implementation uses [addJavascriptInterface](https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface(java.lang.Object,%20java.lang.String)). + ///The iOS implementation uses [addScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537172-addscriptmessagehandler?language=objc) + /// + ///The JavaScript function that can be used to call the handler is `window.flutter_inappbrowser.callHandler(handlerName <String>, ...args);`, where `args` are [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters). + ///The `args` will be stringified automatically using `JSON.stringify(args)` method and then they will be decoded on the Dart side. + int addJavaScriptHandler(String handlerName, JavaScriptHandlerCallback callback) { + this.javaScriptHandlersMap.putIfAbsent(handlerName, () => List<JavaScriptHandlerCallback>()); + this.javaScriptHandlersMap[handlerName].add(callback); + return this.javaScriptHandlersMap[handlerName].indexOf(callback); + } + + ///Removes a JavaScript message handler previously added with the [addJavaScriptHandler()] method in the [handlerName] list by its position [index]. + ///Returns `true` if the callback is removed, otherwise `false`. + bool removeJavaScriptHandler(String handlerName, int index) { + try { + this.javaScriptHandlersMap[handlerName].removeAt(index); + return true; + } + on RangeError catch(e) { + print(e); + } + return false; + } + ///Event fires when the [InAppBrowser] starts to load an [url]. void onLoadStart(String url) { @@ -363,12 +409,14 @@ class InAppBrowser { } ///Give the host application a chance to take control when a URL is about to be loaded in the current WebView. - ///In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`. + ///**NOTE**: In order to be able to listen this event, you need to set `useShouldOverrideUrlLoading` option to `true`. void shouldOverrideUrlLoading(String url) { } - ///Event fires when the [InAppBrowser] webview will load the resource specified by the given [WebResourceRequest]. + ///Event fires when the [InAppBrowser] webview loads a resource. + ///**NOTE**: In order to be able to listen this event, you need to set `useOnLoadResource` option to `true`. + ///**NOTE only for iOS**: In some cases, the [response.data] of a [response] with `text/html` encoding could be empty. void onLoadResource(WebResourceResponse response, WebResourceRequest request) { }