From b7c60993889b607f8371331b016760babb0f2366 Mon Sep 17 00:00:00 2001 From: Volodymyr Taliar Date: Wed, 30 Jun 2021 17:28:33 +0300 Subject: [PATCH] Added InAppBrowser plugin --- .eslintrc.yml | 23 + .gitattributes | 26 + .gitignore | 16 + .npmignore | 3 + LICENSE | 212 +++ NOTICE | 5 + README.md | 32 + package.json | 60 + plugin.xml | 101 ++ scripts/hooks/afterPluginInstall.js | 17 + src/android/InAppBrowser.java | 1497 +++++++++++++++++ src/android/InAppBrowserDialog.java | 57 + src/android/InAppChromeClient.java | 186 ++ .../res/drawable-hdpi/ic_action_next_item.png | Bin 0 -> 593 bytes .../drawable-hdpi/ic_action_previous_item.png | Bin 0 -> 599 bytes .../res/drawable-hdpi/ic_action_remove.png | Bin 0 -> 438 bytes .../res/drawable-mdpi/ic_action_next_item.png | Bin 0 -> 427 bytes .../drawable-mdpi/ic_action_previous_item.png | Bin 0 -> 438 bytes .../res/drawable-mdpi/ic_action_remove.png | Bin 0 -> 328 bytes .../drawable-xhdpi/ic_action_next_item.png | Bin 0 -> 727 bytes .../ic_action_previous_item.png | Bin 0 -> 744 bytes .../res/drawable-xhdpi/ic_action_remove.png | Bin 0 -> 536 bytes .../drawable-xxhdpi/ic_action_next_item.png | Bin 0 -> 1021 bytes .../ic_action_previous_item.png | Bin 0 -> 1038 bytes .../res/drawable-xxhdpi/ic_action_remove.png | Bin 0 -> 681 bytes src/ios/CDVInAppBrowserNavigationController.h | 27 + src/ios/CDVInAppBrowserNavigationController.m | 63 + src/ios/CDVInAppBrowserOptions.h | 50 + src/ios/CDVInAppBrowserOptions.m | 90 + src/ios/CDVWKInAppBrowser.h | 80 + src/ios/CDVWKInAppBrowser.m | 1380 +++++++++++++++ src/ios/CDVWKInAppBrowserUIDelegate.h | 32 + src/ios/CDVWKInAppBrowserUIDelegate.m | 127 ++ tests/.eslintrc.yml | 2 + tests/package.json | 14 + tests/plugin.xml | 33 + tests/resources/inject.css | 21 + tests/resources/inject.html | 44 + tests/resources/inject.js | 21 + tests/resources/local.html | 67 + tests/resources/local.pdf | Bin 0 -> 8568 bytes tests/resources/video.html | 45 + tests/tests.js | 1018 +++++++++++ types/index.d.ts | 109 ++ www/inappbrowser.css | 114 ++ www/inappbrowser.js | 119 ++ 46 files changed, 5691 insertions(+) create mode 100644 .eslintrc.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 package.json create mode 100644 plugin.xml create mode 100644 scripts/hooks/afterPluginInstall.js create mode 100644 src/android/InAppBrowser.java create mode 100644 src/android/InAppBrowserDialog.java create mode 100644 src/android/InAppChromeClient.java create mode 100644 src/android/res/drawable-hdpi/ic_action_next_item.png create mode 100644 src/android/res/drawable-hdpi/ic_action_previous_item.png create mode 100644 src/android/res/drawable-hdpi/ic_action_remove.png create mode 100644 src/android/res/drawable-mdpi/ic_action_next_item.png create mode 100644 src/android/res/drawable-mdpi/ic_action_previous_item.png create mode 100644 src/android/res/drawable-mdpi/ic_action_remove.png create mode 100644 src/android/res/drawable-xhdpi/ic_action_next_item.png create mode 100644 src/android/res/drawable-xhdpi/ic_action_previous_item.png create mode 100644 src/android/res/drawable-xhdpi/ic_action_remove.png create mode 100644 src/android/res/drawable-xxhdpi/ic_action_next_item.png create mode 100644 src/android/res/drawable-xxhdpi/ic_action_previous_item.png create mode 100644 src/android/res/drawable-xxhdpi/ic_action_remove.png create mode 100644 src/ios/CDVInAppBrowserNavigationController.h create mode 100644 src/ios/CDVInAppBrowserNavigationController.m create mode 100644 src/ios/CDVInAppBrowserOptions.h create mode 100644 src/ios/CDVInAppBrowserOptions.m create mode 100644 src/ios/CDVWKInAppBrowser.h create mode 100644 src/ios/CDVWKInAppBrowser.m create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.h create mode 100644 src/ios/CDVWKInAppBrowserUIDelegate.m create mode 100644 tests/.eslintrc.yml create mode 100644 tests/package.json create mode 100644 tests/plugin.xml create mode 100644 tests/resources/inject.css create mode 100644 tests/resources/inject.html create mode 100644 tests/resources/inject.js create mode 100644 tests/resources/local.html create mode 100644 tests/resources/local.pdf create mode 100644 tests/resources/video.html create mode 100644 tests/tests.js create mode 100644 types/index.d.ts create mode 100644 www/inappbrowser.css create mode 100644 www/inappbrowser.js diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..17277f7 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +root: true +extends: '@cordova/eslint-config/browser' + +overrides: + - files: [tests/**/*.js] + extends: '@cordova/eslint-config/node-tests' diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfbd1e5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,26 @@ +* text eol=lf + +# +## These files are binary and should be left untouched +# + +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.zip binary +*.7z binary +*.ttf binary +*.eot binary +*.woff binary +*.pyc binary +*.pdf binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b41881 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +#If ignorance is bliss, then somebody knock the smile off my face + +*.csproj.user +*.suo +*.cache +Thumbs.db +*.DS_Store + +*.bak +*.cache +*.log +*.swp +*.user +*.idea + +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..45e3c38 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.* +appveyor.yml +tests diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc57d7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,212 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== + +NOTICE content is from: https://github.com/apache/cordova-plugin-inappbrowser/blob/master/NOTICE + +Apache Cordova +Copyright 2012 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..8ec56a5 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache Cordova +Copyright 2012 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d87ff1 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# cordova-plugin-bbd-inappbrowser + +BlackBerry Dynamics Cordova InAppBrowser plugin. It is a fork of [cordova-plugin-inappbrowser@5.0.0](https://github.com/apache/cordova-plugin-inappbrowser/tree/5.0.x). + +This plugin enables you to securely load helpful articles, videos, and web resources inside of your app without leaving your app. + +## Preconditions + +`cordova-plugin-bbd-inappbrowser` is dependent on `cordova-plugin-bbd-base` plugin. + +## Installation + +`cordova plugin add git+https://github.com/blackberry/blackberry-dynamics-cordova-plugins#inappbrowser` + +## Uninstallation + +`cordova plugin rm cordova-plugin-bbd-inappbrowser` + +# Supported Platforms + - Android + - iOS + +# Usage +Please take a look at the original examples [here](https://github.com/apache/cordova-plugin-inappbrowser/tree/5.0.x#example-1) or visit Dynamics Cordova `InAppBrowser` sample application on [BlackBerry GitHub](https://github.com/blackberry/BlackBerry-Dynamics-Cordova-Samples/). + +## License + +Apache 2.0 License + +## Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..8d8c12f --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "cordova-plugin-bbd-inappbrowser", + "version": "1.0.0", + "description": "BlackBerry Dynamics Cordova InAppBrowser Plugin", + "license": "SEE LICENSE IN LICENSE", + "homepage": "https://developers.blackberry.com", + "types": "./types/index.d.ts", + "cordova": { + "id": "https://github.com/blackberry/BlackBerry-Dynamics-Cordova-Plugins/tree/inappbrowser", + "platforms": [ + "android", + "ios" + ] + }, + "repository": { + "name": "cordova-plugin-bbd-inappbrowser", + "version": "1.0.0", + "private": true, + "url": "https://github.com/blackberry/BlackBerry-Dynamics-Cordova-Plugins/tree/inappbrowser" + }, + "bugs": "https://github.com/blackberry/BlackBerry-Dynamics-Cordova-Plugins/tree/inappbrowser/issues", + "keywords": [ + "cordova", + "in", + "app", + "browser", + "inappbrowser", + "ecosystem:cordova", + "cordova-android", + "cordova-ios" + ], + "scripts": { + "test": "npm run lint", + "lint": "eslint ." + }, + "engines": { + "cordovaDependencies": { + "0.2.3": { + "cordova": ">=3.1.0" + }, + "4.0.0": { + "cordova": ">=3.1.0", + "cordova-ios": ">=4.0.0" + }, + "5.0.0": { + "cordova-android": ">=9.0.0", + "cordova-ios": ">=6.0.0", + "cordova": ">=9.0.0" + }, + "6.0.0": { + "cordova": ">100" + } + } + }, + "author": "Apache Software Foundation", + "contributors": "Bohdan Pidluzhnyy , Volodymyr Taliar ", + "devDependencies": { + "@cordova/eslint-config": "^3.0.0" + } +} diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..731f55d --- /dev/null +++ b/plugin.xml @@ -0,0 +1,101 @@ + + + + + + cordova-plugin-bbd-inappbrowser + Cordova InAppBrowser Plugin + SEE LICENSE IN LICENSE + cordova,in,app,browser,inappbrowser + + + The InAppBrowser plugin is dependent on Base plugin, so please make sure the Base plugin is installed correctly. + + Original Cordova InAppBrowser plugin link: https://github.com/apache/cordova-plugin-inappbrowser + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/hooks/afterPluginInstall.js b/scripts/hooks/afterPluginInstall.js new file mode 100644 index 0000000..a2433f5 --- /dev/null +++ b/scripts/hooks/afterPluginInstall.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2021 BlackBerry Limited. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + console.log('\x1b[32m%s\x1b[0m','Plugin cordova-plugin-bbd-inappbrowser was successfully added.'); \ No newline at end of file diff --git a/src/android/InAppBrowser.java b/src/android/InAppBrowser.java new file mode 100644 index 0000000..8c6f21e --- /dev/null +++ b/src/android/InAppBrowser.java @@ -0,0 +1,1497 @@ +/* + Copyright (c) 2021 BlackBerry Limited. All Rights Reserved. + Some modifications to the original cordova-plugin-inappbrowser + from https://github.com/apache/cordova-plugin-inappbrowser/ + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.inappbrowser; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Parcelable; +import android.provider.Browser; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.graphics.Color; +import android.net.http.SslError; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.InputType; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.webkit.CookieManager; +import android.webkit.HttpAuthHandler; +import android.webkit.JavascriptInterface; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.good.gd.widget.GDTextView; +import com.good.gd.widget.GDEditText; + +import com.good.gd.cordova.core.webview.BBWebView; +import com.good.gd.cordova.core.webview.BBWebViewClient; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.Config; +import org.apache.cordova.CordovaArgs; +import org.apache.cordova.CordovaHttpAuthHandler; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginManager; +import org.apache.cordova.PluginResult; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.HashMap; +import java.util.StringTokenizer; + +@SuppressLint("SetJavaScriptEnabled") +public class InAppBrowser extends CordovaPlugin { + + private static final String NULL = "null"; + protected static final String LOG_TAG = "InAppBrowser"; + private static final String SELF = "_self"; + private static final String SYSTEM = "_system"; + private static final String EXIT_EVENT = "exit"; + private static final String LOCATION = "location"; + private static final String ZOOM = "zoom"; + private static final String HIDDEN = "hidden"; + private static final String LOAD_START_EVENT = "loadstart"; + private static final String LOAD_STOP_EVENT = "loadstop"; + private static final String LOAD_ERROR_EVENT = "loaderror"; + private static final String MESSAGE_EVENT = "message"; + private static final String CLEAR_ALL_CACHE = "clearcache"; + private static final String CLEAR_SESSION_CACHE = "clearsessioncache"; + private static final String HARDWARE_BACK_BUTTON = "hardwareback"; + private static final String MEDIA_PLAYBACK_REQUIRES_USER_ACTION = "mediaPlaybackRequiresUserAction"; + private static final String SHOULD_PAUSE = "shouldPauseOnSuspend"; + private static final Boolean DEFAULT_HARDWARE_BACK = true; + private static final String USER_WIDE_VIEW_PORT = "useWideViewPort"; + private static final String TOOLBAR_COLOR = "toolbarcolor"; + private static final String CLOSE_BUTTON_CAPTION = "closebuttoncaption"; + private static final String CLOSE_BUTTON_COLOR = "closebuttoncolor"; + private static final String LEFT_TO_RIGHT = "lefttoright"; + private static final String HIDE_NAVIGATION = "hidenavigationbuttons"; + private static final String NAVIGATION_COLOR = "navigationbuttoncolor"; + private static final String HIDE_URL = "hideurlbar"; + private static final String FOOTER = "footer"; + private static final String FOOTER_COLOR = "footercolor"; + private static final String BEFORELOAD = "beforeload"; + private static final String FULLSCREEN = "fullscreen"; + + private static final List customizableOptions = Arrays.asList(CLOSE_BUTTON_CAPTION, TOOLBAR_COLOR, NAVIGATION_COLOR, + CLOSE_BUTTON_COLOR, FOOTER_COLOR); + + private InAppBrowserDialog dialog; + + private BBWebView inAppWebView; + + private GDEditText edittext; + private CallbackContext callbackContext; + private boolean showLocationBar = true; + private boolean showZoomControls = true; + private boolean openWindowHidden = false; + private boolean clearAllCache = false; + private boolean clearSessionCache = false; + private boolean hadwareBackButton = true; + private boolean mediaPlaybackRequiresUserGesture = false; + private boolean shouldPauseInAppBrowser = false; + private boolean useWideViewPort = true; + private ValueCallback mUploadCallback; + private final static int FILECHOOSER_REQUESTCODE = 1; + private String closeButtonCaption = ""; + private String closeButtonColor = ""; + private boolean leftToRight = false; + private int toolbarColor = android.graphics.Color.LTGRAY; + private boolean hideNavigationButtons = false; + private String navigationButtonColor = ""; + private boolean hideUrlBar = false; + private boolean showFooter = false; + private String footerColor = ""; + private String beforeload = ""; + private boolean fullscreen = true; + private String[] allowedSchemes; + private BBWebViewClient currentClient; + + /** + * Executes the request and returns PluginResult. + * + * @param action the action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackContext the callbackContext used when calling back into + * JavaScript. + * @return A PluginResult object with a status and message. + */ + public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) + throws JSONException { + if (action.equals("open")) { + this.callbackContext = callbackContext; + final String url = args.getString(0); + String t = args.optString(1); + if (t == null || t.equals("") || t.equals(NULL)) { + t = SELF; + } + final String target = t; + final HashMap features = parseFeature(args.optString(2)); + + LOG.d(LOG_TAG, "target = " + target); + + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + String result = ""; + // SELF + if (SELF.equals(target)) { + LOG.d(LOG_TAG, "in self"); + /* + * This code exists for compatibility between 3.x and 4.x versions of Cordova. + * Previously the Config class had a static method, isUrlWhitelisted(). That + * responsibility has been moved to the plugins, with an aggregating method in + * PluginManager. + */ + Boolean shouldAllowNavigation = null; + if (url.startsWith("javascript:")) { + shouldAllowNavigation = true; + } + if (shouldAllowNavigation == null) { + try { + Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); + shouldAllowNavigation = (Boolean) iuw.invoke(null, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + if (shouldAllowNavigation == null) { + try { + Method gpm = webView.getClass().getMethod("getPluginManager"); + PluginManager pm = (PluginManager) gpm.invoke(webView); + Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); + shouldAllowNavigation = (Boolean) san.invoke(pm, url); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + // load in webview + if (Boolean.TRUE.equals(shouldAllowNavigation)) { + LOG.d(LOG_TAG, "loading in webview"); + webView.loadUrl(url); + } + // Load the dialer + else if (url.startsWith(WebView.SCHEME_TEL)) { + try { + LOG.d(LOG_TAG, "loading in dialer"); + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } + // load in InAppBrowser + else { + LOG.d(LOG_TAG, "loading in InAppBrowser"); + result = showWebPage(url, features); + } + } + // SYSTEM + else if (SYSTEM.equals(target)) { + LOG.d(LOG_TAG, "in system"); + result = openExternal(url); + } + // BLANK - or anything else + else { + LOG.d(LOG_TAG, "in blank"); + result = showWebPage(url, features); + } + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } + }); + } else if (action.equals("close")) { + closeDialog(); + } else if (action.equals("loadAfterBeforeload")) { + if (beforeload == null) { + LOG.e(LOG_TAG, "unexpected loadAfterBeforeload called without feature beforeload=yes"); + } + final String url = args.getString(0); + this.cordova.getActivity().runOnUiThread(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + inAppWebView.loadUrl(url); + } + }); + } else if (action.equals("injectScriptCode")) { + String jsWrapper = null; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(){prompt(JSON.stringify([eval(%%s)]), 'gap-iab://%s')})()", + callbackContext.getCallbackId()); + } + injectDeferredObject(args.getString(0), jsWrapper); + } else if (action.equals("injectScriptFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format( + "(function(d) { var c = d.createElement('script'); c.src = %%s; c.onload = function() { prompt('', 'gap-iab://%s'); }; d.body.appendChild(c); })(document)", + callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } else if (action.equals("injectStyleCode")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format( + "(function(d) { var c = d.createElement('style'); c.innerHTML = %%s; d.body.appendChild(c); prompt('', 'gap-iab://%s');})(document)", + callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('style'); c.innerHTML = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } else if (action.equals("injectStyleFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format( + "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%s; d.head.appendChild(c); prompt('', 'gap-iab://%s');})(document)", + callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %s; d.head.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } else if (action.equals("show")) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (dialog != null && !cordova.getActivity().isFinishing()) { + dialog.show(); + } + } + }); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); + } else if (action.equals("hide")) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (dialog != null && !cordova.getActivity().isFinishing()) { + dialog.hide(); + } + } + }); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); + } else { + return false; + } + return true; + } + + /** + * Called when the view navigates. + */ + @Override + public void onReset() { + closeDialog(); + } + + /** + * Called when the system is about to start resuming a previous activity. + */ + @Override + public void onPause(boolean multitasking) { + if (shouldPauseInAppBrowser) { + inAppWebView.onPause(); + } + } + + /** + * Called when the activity will start interacting with the user. + */ + @Override + public void onResume(boolean multitasking) { + if (shouldPauseInAppBrowser) { + inAppWebView.onResume(); + } + } + + /** + * Called by AccelBroker when listener is to be shut down. Stop listener. + */ + public void onDestroy() { + closeDialog(); + } + + /** + * Inject an object (script or style) into the InAppBrowser WebView. + * + * This is a helper method for the inject{Script|Style}{Code|File} API calls, + * which provides a consistent method for injecting JavaScript code into the + * document. + * + * If a wrapper string is supplied, then the source string will be JSON-encoded + * (adding quotes) and wrapped using string formatting. (The wrapper string + * should have a single '%s' marker) + * + * @param source The source object (filename or script/style text) to inject + * into the document. + * @param jsWrapper A JavaScript string to wrap the source string in, so that + * the object is properly injected, or null if the source + * string is JavaScript text which should be executed directly. + */ + private void injectDeferredObject(String source, String jsWrapper) { + if (inAppWebView != null) { + String scriptToInject; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length() - 1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } else { + scriptToInject = source; + } + final String finalScriptToInject = scriptToInject; + this.cordova.getActivity().runOnUiThread(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + inAppWebView.evaluateJavascript(finalScriptToInject, null); + } + }); + } else { + LOG.d(LOG_TAG, "Can't inject code into the system browser"); + } + } + + /** + * Put the list of features into a hash map + * + * @param optString + * @return + */ + private HashMap parseFeature(String optString) { + if (optString.equals(NULL)) { + return null; + } else { + HashMap map = new HashMap(); + StringTokenizer features = new StringTokenizer(optString, ","); + StringTokenizer option; + while (features.hasMoreElements()) { + option = new StringTokenizer(features.nextToken(), "="); + if (option.hasMoreElements()) { + String key = option.nextToken(); + String value = option.nextToken(); + if (!customizableOptions.contains(key)) { + value = value.equals("yes") || value.equals("no") ? value : "yes"; + } + map.put(key, value); + } + } + return map; + } + } + + /** + * Display a new browser with the specified URL. + * + * @param url the url to load. + * @return "" if ok, or error message. + */ + public String openExternal(String url) { + try { + Intent intent = null; + intent = new Intent(Intent.ACTION_VIEW); + // Omitting the MIME type for file: URLs causes "No Activity found to handle + // Intent". + // Adding the MIME type to http: URLs causes them to not be handled by the + // downloader. + Uri uri = Uri.parse(url); + if ("file".equals(uri.getScheme())) { + intent.setDataAndType(uri, webView.getResourceApi().getMimeType(uri)); + } else { + intent.setData(uri); + } + intent.putExtra(Browser.EXTRA_APPLICATION_ID, cordova.getActivity().getPackageName()); + // CB-10795: Avoid circular loops by preventing it from opening in the current + // app + this.openExternalExcludeCurrentApp(intent); + return ""; + // not catching FileUriExposedException explicitly because buildtools<24 doesn't + // know about it + } catch (java.lang.RuntimeException e) { + LOG.d(LOG_TAG, "InAppBrowser: Error loading url " + url + ":" + e.toString()); + return e.toString(); + } + } + + /** + * Opens the intent, providing a chooser that excludes the current app to avoid + * circular loops. + */ + private void openExternalExcludeCurrentApp(Intent intent) { + String currentPackage = cordova.getActivity().getPackageName(); + boolean hasCurrentPackage = false; + + PackageManager pm = cordova.getActivity().getPackageManager(); + List activities = pm.queryIntentActivities(intent, 0); + ArrayList targetIntents = new ArrayList(); + + for (ResolveInfo ri : activities) { + if (!currentPackage.equals(ri.activityInfo.packageName)) { + Intent targetIntent = (Intent) intent.clone(); + targetIntent.setPackage(ri.activityInfo.packageName); + targetIntents.add(targetIntent); + } else { + hasCurrentPackage = true; + } + } + + // If the current app package isn't a target for this URL, then use + // the normal launch behavior + if (hasCurrentPackage == false || targetIntents.size() == 0) { + this.cordova.getActivity().startActivity(intent); + } + // If there's only one possible intent, launch it directly + else if (targetIntents.size() == 1) { + this.cordova.getActivity().startActivity(targetIntents.get(0)); + } + // Otherwise, show a custom chooser without the current app listed + else if (targetIntents.size() > 0) { + Intent chooser = Intent.createChooser(targetIntents.remove(targetIntents.size() - 1), null); + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[] {})); + this.cordova.getActivity().startActivity(chooser); + } + } + + /** + * Closes the dialog + */ + public void closeDialog() { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + final WebView childView = inAppWebView; + // The JS protects against multiple calls, so this should happen only when + // closeDialog() is called by other native code. + if (childView == null) { + return; + } + + if (dialog != null && !cordova.getActivity().isFinishing()) { + dialog.dismiss(); + dialog = null; + } + + // NB: From SDK 19: "If you call methods on WebView from any thread + // other than your app's UI thread, it can cause unexpected results." + // http://developer.android.com/guide/webapps/migrating.html#Threads + childView.loadUrl("about:blank"); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", EXIT_EVENT); + sendUpdate(obj, false); + } catch (JSONException ex) { + LOG.d(LOG_TAG, "Should never happen"); + } + } + }); + } + + /** + * Checks to see if it is possible to go back one page in history, then does so. + */ + public void goBack() { + if (this.inAppWebView.canGoBack()) { + this.inAppWebView.goBack(); + } + } + + /** + * Can the web browser go back? + * + * @return boolean + */ + public boolean canGoBack() { + return this.inAppWebView.canGoBack(); + } + + /** + * Has the user set the hardware back button to go back + * + * @return boolean + */ + public boolean hardwareBack() { + return hadwareBackButton; + } + + /** + * Checks to see if it is possible to go forward one page in history, then does + * so. + */ + private void goForward() { + if (this.inAppWebView.canGoForward()) { + this.inAppWebView.goForward(); + } + } + + /** + * Navigate to the new page + * + * @param url to load + */ + private void navigate(String url) { + InputMethodManager imm = (InputMethodManager) this.cordova.getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(edittext.getWindowToken(), 0); + + if (!url.startsWith("http") && !url.startsWith("file:")) { + this.inAppWebView.loadUrl("http://" + url); + } else { + this.inAppWebView.loadUrl(url); + } + this.inAppWebView.requestFocus(); + } + + /** + * Should we show the location bar? + * + * @return boolean + */ + private boolean getShowLocationBar() { + return this.showLocationBar; + } + + private InAppBrowser getInAppBrowser() { + return this; + } + + /** + * Display a new browser with the specified URL. + * + * @param url the url to load. + * @param features jsonObject + */ + public String showWebPage(final String url, HashMap features) { + // Determine if we should hide the location bar. + showLocationBar = true; + showZoomControls = true; + openWindowHidden = false; + mediaPlaybackRequiresUserGesture = false; + + if (features != null) { + String show = features.get(LOCATION); + if (show != null) { + showLocationBar = show.equals("yes") ? true : false; + } + if (showLocationBar) { + String hideNavigation = features.get(HIDE_NAVIGATION); + String hideUrl = features.get(HIDE_URL); + if (hideNavigation != null) + hideNavigationButtons = hideNavigation.equals("yes") ? true : false; + if (hideUrl != null) + hideUrlBar = hideUrl.equals("yes") ? true : false; + } + String zoom = features.get(ZOOM); + if (zoom != null) { + showZoomControls = zoom.equals("yes") ? true : false; + } + String hidden = features.get(HIDDEN); + if (hidden != null) { + openWindowHidden = hidden.equals("yes") ? true : false; + } + String hardwareBack = features.get(HARDWARE_BACK_BUTTON); + if (hardwareBack != null) { + hadwareBackButton = hardwareBack.equals("yes") ? true : false; + } else { + hadwareBackButton = DEFAULT_HARDWARE_BACK; + } + String mediaPlayback = features.get(MEDIA_PLAYBACK_REQUIRES_USER_ACTION); + if (mediaPlayback != null) { + mediaPlaybackRequiresUserGesture = mediaPlayback.equals("yes") ? true : false; + } + String cache = features.get(CLEAR_ALL_CACHE); + if (cache != null) { + clearAllCache = cache.equals("yes") ? true : false; + } else { + cache = features.get(CLEAR_SESSION_CACHE); + if (cache != null) { + clearSessionCache = cache.equals("yes") ? true : false; + } + } + String shouldPause = features.get(SHOULD_PAUSE); + if (shouldPause != null) { + shouldPauseInAppBrowser = shouldPause.equals("yes") ? true : false; + } + String wideViewPort = features.get(USER_WIDE_VIEW_PORT); + if (wideViewPort != null) { + useWideViewPort = wideViewPort.equals("yes") ? true : false; + } + String closeButtonCaptionSet = features.get(CLOSE_BUTTON_CAPTION); + if (closeButtonCaptionSet != null) { + closeButtonCaption = closeButtonCaptionSet; + } + String closeButtonColorSet = features.get(CLOSE_BUTTON_COLOR); + if (closeButtonColorSet != null) { + closeButtonColor = closeButtonColorSet; + } + String leftToRightSet = features.get(LEFT_TO_RIGHT); + leftToRight = leftToRightSet != null && leftToRightSet.equals("yes"); + + String toolbarColorSet = features.get(TOOLBAR_COLOR); + if (toolbarColorSet != null) { + toolbarColor = android.graphics.Color.parseColor(toolbarColorSet); + } + String navigationButtonColorSet = features.get(NAVIGATION_COLOR); + if (navigationButtonColorSet != null) { + navigationButtonColor = navigationButtonColorSet; + } + String showFooterSet = features.get(FOOTER); + if (showFooterSet != null) { + showFooter = showFooterSet.equals("yes") ? true : false; + } + String footerColorSet = features.get(FOOTER_COLOR); + if (footerColorSet != null) { + footerColor = footerColorSet; + } + if (features.get(BEFORELOAD) != null) { + beforeload = features.get(BEFORELOAD); + } + String fullscreenSet = features.get(FULLSCREEN); + if (fullscreenSet != null) { + fullscreen = fullscreenSet.equals("yes") ? true : false; + } + } + + final CordovaWebView thatWebView = this.webView; + + // Create dialog in new thread + Runnable runnable = new Runnable() { + /** + * Convert our DIP units to Pixels + * + * @return int + */ + private int dpToPixels(int dipValue) { + int value = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) dipValue, + cordova.getActivity().getResources().getDisplayMetrics()); + + return value; + } + + private View createCloseButton(int id) { + View _close; + Resources activityRes = cordova.getActivity().getResources(); + + if (closeButtonCaption != "") { + // Use TextView for text + GDTextView close = new GDTextView(cordova.getActivity()); + close.setText(closeButtonCaption); + close.setTextSize(20); + if (closeButtonColor != "") + close.setTextColor(android.graphics.Color.parseColor(closeButtonColor)); + close.setGravity(android.view.Gravity.CENTER_VERTICAL); + close.setPadding(this.dpToPixels(10), 0, this.dpToPixels(10), 0); + _close = close; + } else { + ImageButton close = new ImageButton(cordova.getActivity()); + int closeResId = activityRes.getIdentifier("ic_action_remove", "drawable", + cordova.getActivity().getPackageName()); + Drawable closeIcon = activityRes.getDrawable(closeResId); + if (closeButtonColor != "") + close.setColorFilter(android.graphics.Color.parseColor(closeButtonColor)); + close.setImageDrawable(closeIcon); + close.setScaleType(ImageView.ScaleType.FIT_CENTER); + close.getAdjustViewBounds(); + + _close = close; + } + + RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + if (leftToRight) + closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + else + closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + _close.setLayoutParams(closeLayoutParams); + _close.setBackground(null); + + _close.setContentDescription("Close Button"); + _close.setId(Integer.valueOf(id)); + _close.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + closeDialog(); + } + }); + + return _close; + } + + @SuppressLint("NewApi") + public void run() { + + // CB-6702 InAppBrowser hangs when opening more than one instance + if (dialog != null) { + dialog.dismiss(); + } + ; + + // Let's create the main dialog + dialog = new InAppBrowserDialog(cordova.getActivity(), android.R.style.Theme_NoTitleBar); + dialog.getWindow().getAttributes().windowAnimations = android.R.style.Animation_Dialog; + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + if (fullscreen) { + dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + dialog.setCancelable(true); + dialog.setInAppBroswer(getInAppBrowser()); + + // Main container layout + LinearLayout main = new LinearLayout(cordova.getActivity()); + main.setOrientation(LinearLayout.VERTICAL); + + // Toolbar layout + RelativeLayout toolbar = new RelativeLayout(cordova.getActivity()); + // Please, no more black! + toolbar.setBackgroundColor(toolbarColor); + toolbar.setLayoutParams( + new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, this.dpToPixels(44))); + toolbar.setPadding(this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2)); + if (leftToRight) { + toolbar.setHorizontalGravity(Gravity.LEFT); + } else { + toolbar.setHorizontalGravity(Gravity.RIGHT); + } + toolbar.setVerticalGravity(Gravity.TOP); + + // Action Button Container layout + RelativeLayout actionButtonContainer = new RelativeLayout(cordova.getActivity()); + RelativeLayout.LayoutParams actionButtonLayoutParams = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + if (leftToRight) + actionButtonLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + else + actionButtonLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + actionButtonContainer.setLayoutParams(actionButtonLayoutParams); + actionButtonContainer.setHorizontalGravity(Gravity.LEFT); + actionButtonContainer.setVerticalGravity(Gravity.CENTER_VERTICAL); + actionButtonContainer.setId(leftToRight ? Integer.valueOf(5) : Integer.valueOf(1)); + + // Back button + ImageButton back = new ImageButton(cordova.getActivity()); + RelativeLayout.LayoutParams backLayoutParams = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT); + back.setLayoutParams(backLayoutParams); + back.setContentDescription("Back Button"); + back.setId(Integer.valueOf(2)); + Resources activityRes = cordova.getActivity().getResources(); + int backResId = activityRes.getIdentifier("ic_action_previous_item", "drawable", + cordova.getActivity().getPackageName()); + Drawable backIcon = activityRes.getDrawable(backResId); + if (navigationButtonColor != "") + back.setColorFilter(android.graphics.Color.parseColor(navigationButtonColor)); + back.setBackground(null); + back.setImageDrawable(backIcon); + back.setScaleType(ImageView.ScaleType.FIT_CENTER); + back.setPadding(0, this.dpToPixels(10), 0, this.dpToPixels(10)); + back.getAdjustViewBounds(); + + back.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + goBack(); + } + }); + + // Forward button + ImageButton forward = new ImageButton(cordova.getActivity()); + RelativeLayout.LayoutParams forwardLayoutParams = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2); + forward.setLayoutParams(forwardLayoutParams); + forward.setContentDescription("Forward Button"); + forward.setId(Integer.valueOf(3)); + int fwdResId = activityRes.getIdentifier("ic_action_next_item", "drawable", + cordova.getActivity().getPackageName()); + Drawable fwdIcon = activityRes.getDrawable(fwdResId); + if (navigationButtonColor != "") + forward.setColorFilter(android.graphics.Color.parseColor(navigationButtonColor)); + forward.setBackground(null); + forward.setImageDrawable(fwdIcon); + forward.setScaleType(ImageView.ScaleType.FIT_CENTER); + forward.setPadding(0, this.dpToPixels(10), 0, this.dpToPixels(10)); + forward.getAdjustViewBounds(); + + forward.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + goForward(); + } + }); + + // Edit Text Box + edittext = new GDEditText(cordova.getActivity()); + RelativeLayout.LayoutParams textLayoutParams = new RelativeLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + textLayoutParams.addRule(RelativeLayout.RIGHT_OF, 1); + textLayoutParams.addRule(RelativeLayout.LEFT_OF, 5); + edittext.setLayoutParams(textLayoutParams); + edittext.setId(Integer.valueOf(4)); + edittext.setSingleLine(true); + edittext.setText(url); + edittext.setInputType(InputType.TYPE_TEXT_VARIATION_URI); + edittext.setImeOptions(EditorInfo.IME_ACTION_GO); + edittext.setInputType(InputType.TYPE_NULL); // Will not except input... Makes the text NON-EDITABLE + edittext.setOnKeyListener(new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + // If the event is a key-down event on the "enter" button + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + navigate(edittext.getText().toString()); + return true; + } + return false; + } + }); + + // Header Close/Done button + int closeButtonId = leftToRight ? 1 : 5; + View close = createCloseButton(closeButtonId); + toolbar.addView(close); + + // Footer + RelativeLayout footer = new RelativeLayout(cordova.getActivity()); + int _footerColor; + if (footerColor != "") { + _footerColor = Color.parseColor(footerColor); + } else { + _footerColor = android.graphics.Color.LTGRAY; + } + footer.setBackgroundColor(_footerColor); + RelativeLayout.LayoutParams footerLayout = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, + this.dpToPixels(44)); + footerLayout.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE); + footer.setLayoutParams(footerLayout); + if (closeButtonCaption != "") + footer.setPadding(this.dpToPixels(8), this.dpToPixels(8), this.dpToPixels(8), this.dpToPixels(8)); + footer.setHorizontalGravity(Gravity.LEFT); + footer.setVerticalGravity(Gravity.BOTTOM); + + View footerClose = createCloseButton(7); + footer.addView(footerClose); + + // WebView + inAppWebView = new BBWebView(cordova.getActivity()); + + inAppWebView.setLayoutParams( + new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + inAppWebView.setId(Integer.valueOf(6)); + + currentClient = new BBWebViewClient(); + inAppWebView.setWebViewClient(currentClient); + WebSettings settings = inAppWebView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setBuiltInZoomControls(showZoomControls); + settings.setPluginState(android.webkit.WebSettings.PluginState.ON); + + // File Chooser Implemented ChromeClient + inAppWebView.setWebChromeClient(new InAppChromeClient(currentClient.getObserver(), thatWebView) { + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, + WebChromeClient.FileChooserParams fileChooserParams) { + LOG.d(LOG_TAG, "File Chooser 5.0+"); + // If callback exists, finish it. + if (mUploadCallback != null) { + mUploadCallback.onReceiveValue(null); + } + mUploadCallback = filePathCallback; + + // Create File Chooser Intent + Intent content = new Intent(Intent.ACTION_GET_CONTENT); + content.addCategory(Intent.CATEGORY_OPENABLE); + content.setType("*/*"); + + // Run cordova startActivityForResult + cordova.startActivityForResult(InAppBrowser.this, Intent.createChooser(content, "Select File"), + FILECHOOSER_REQUESTCODE); + return true; + } + }); + + // Add postMessage interface + class JsObject { + @JavascriptInterface + public void postMessage(String data) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", MESSAGE_EVENT); + obj.put("data", new JSONObject(data)); + sendUpdate(obj, true); + } catch (JSONException ex) { + LOG.e(LOG_TAG, "data object passed to postMessage has caused a JSON error."); + } + } + } + + settings.setMediaPlaybackRequiresUserGesture(mediaPlaybackRequiresUserGesture); + inAppWebView.addJavascriptInterface(new JsObject(), "cordova_iab"); + + String overrideUserAgent = preferences.getString("OverrideUserAgent", null); + String appendUserAgent = preferences.getString("AppendUserAgent", null); + + if (overrideUserAgent != null) { + settings.setUserAgentString(overrideUserAgent); + } + if (appendUserAgent != null) { + settings.setUserAgentString(settings.getUserAgentString() + appendUserAgent); + } + + // Toggle whether this is enabled or not! + Bundle appSettings = cordova.getActivity().getIntent().getExtras(); + boolean enableDatabase = appSettings == null ? true + : appSettings.getBoolean("InAppBrowserStorageEnabled", true); + if (enableDatabase) { + String databasePath = cordova.getActivity().getApplicationContext() + .getDir("inAppBrowserDB", Context.MODE_PRIVATE).getPath(); + settings.setDatabasePath(databasePath); + settings.setDatabaseEnabled(true); + } + settings.setDomStorageEnabled(true); + + if (clearAllCache) { + CookieManager.getInstance().removeAllCookie(); + } else if (clearSessionCache) { + CookieManager.getInstance().removeSessionCookie(); + } + + // Enable Thirdparty Cookies + CookieManager.getInstance().setAcceptThirdPartyCookies(inAppWebView, true); + + inAppWebView.loadUrl(url); + inAppWebView.setId(Integer.valueOf(6)); + inAppWebView.getSettings().setLoadWithOverviewMode(true); + inAppWebView.getSettings().setUseWideViewPort(useWideViewPort); + // Multiple Windows set to true to mitigate Chromium security bug. + // See: https://bugs.chromium.org/p/chromium/issues/detail?id=1083819 + inAppWebView.getSettings().setSupportMultipleWindows(true); + inAppWebView.requestFocus(); + inAppWebView.requestFocusFromTouch(); + + // Add the back and forward buttons to our action button container layout + actionButtonContainer.addView(back); + actionButtonContainer.addView(forward); + + // Add the views to our toolbar if they haven't been disabled + if (!hideNavigationButtons) + toolbar.addView(actionButtonContainer); + if (!hideUrlBar) + toolbar.addView(edittext); + + // Don't add the toolbar if its been disabled + if (getShowLocationBar()) { + // Add our toolbar to our main view/layout + main.addView(toolbar); + } + + // Add our webview to our main view/layout + RelativeLayout webViewLayout = new RelativeLayout(cordova.getActivity()); + webViewLayout.addView(inAppWebView); + main.addView(webViewLayout); + + // Don't add the footer unless it's been enabled + if (showFooter) { + webViewLayout.addView(footer); + } + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + + if (dialog != null) { + dialog.setContentView(main); + dialog.show(); + dialog.getWindow().setAttributes(lp); + } + // the goal of openhidden is to load the url and not display it + // Show() needs to be called to cause the URL to be loaded + if (openWindowHidden && dialog != null) { + dialog.hide(); + } + } + }; + this.cordova.getActivity().runOnUiThread(runnable); + return ""; + } + + /** + * Create a new plugin success result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + */ + private void sendUpdate(JSONObject obj, boolean keepCallback) { + sendUpdate(obj, keepCallback, PluginResult.Status.OK); + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + * @param status the status code to return to the JavaScript environment + */ + private void sendUpdate(JSONObject obj, boolean keepCallback, PluginResult.Status status) { + if (callbackContext != null) { + PluginResult result = new PluginResult(status, obj); + result.setKeepCallback(keepCallback); + callbackContext.sendPluginResult(result); + if (!keepCallback) { + callbackContext = null; + } + } + } + + /** + * Receive File Data from File Chooser + * + * @param requestCode the requested code from chromeclient + * @param resultCode the result code returned from android system + * @param intent the data from android file chooser + */ + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + LOG.d(LOG_TAG, "onActivityResult"); + // If RequestCode or Callback is Invalid + if (requestCode != FILECHOOSER_REQUESTCODE || mUploadCallback == null) { + super.onActivityResult(requestCode, resultCode, intent); + return; + } + mUploadCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent)); + mUploadCallback = null; + } + + /** + * The webview client receives notifications about appView + */ + public class InAppBrowserClient extends BBWebViewClient { + GDEditText edittext; + CordovaWebView webView; + String beforeload; + boolean waitForBeforeload; + + /** + * Constructor. + * + * @param webView + * @param mEditText + */ + public InAppBrowserClient(CordovaWebView webView, GDEditText mEditText, String beforeload) { + this.webView = webView; + this.edittext = mEditText; + this.beforeload = beforeload; + this.waitForBeforeload = beforeload != null; + } + + /** + * Override the URL that should be loaded + * + * Legacy (deprecated in API 24) For Android 6 and below. + * + * @param webView + * @param url + */ + @SuppressWarnings("deprecation") + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + return shouldOverrideUrlLoading(url, null); + } + + /** + * Override the URL that should be loaded + * + * New (added in API 24) For Android 7 and above. + * + * @param webView + * @param request + */ + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView webView, WebResourceRequest request) { + return shouldOverrideUrlLoading(request.getUrl().toString(), request.getMethod()); + } + + /** + * Override the URL that should be loaded + * + * This handles a small subset of all the URIs that would be encountered. + * + * @param url + * @param method + */ + public boolean shouldOverrideUrlLoading(String url, String method) { + boolean override = false; + boolean useBeforeload = false; + String errorMessage = null; + + if (beforeload.equals("yes") && method == null) { + useBeforeload = true; + } else if (beforeload.equals("yes") + // TODO handle POST requests then this condition can be removed: + && !method.equals("POST")) { + useBeforeload = true; + } else if (beforeload.equals("get") && (method == null || method.equals("GET"))) { + useBeforeload = true; + } else if (beforeload.equals("post") && (method == null || method.equals("POST"))) { + // TODO handle POST requests + errorMessage = "beforeload doesn't yet support POST requests"; + } + + // On first URL change, initiate JS callback. Only after the beforeload event, + // continue. + if (useBeforeload && this.waitForBeforeload) { + if (sendBeforeLoad(url, method)) { + return true; + } + } + + if (errorMessage != null) { + try { + LOG.e(LOG_TAG, errorMessage); + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", url); + obj.put("code", -1); + obj.put("message", errorMessage); + sendUpdate(obj, true, PluginResult.Status.ERROR); + } catch (Exception e) { + LOG.e(LOG_TAG, "Error sending loaderror for " + url + ": " + e.toString()); + } + } + + if (url.startsWith(WebView.SCHEME_TEL)) { + try { + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + override = true; + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } else if (url.startsWith("geo:") || url.startsWith(WebView.SCHEME_MAILTO) || url.startsWith("market:") + || url.startsWith("intent:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + override = true; + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString()); + } + } + // If sms:5551212?body=This is the message + else if (url.startsWith("sms:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + + // Get address + String address = null; + int parmIndex = url.indexOf('?'); + if (parmIndex == -1) { + address = url.substring(4); + } else { + address = url.substring(4, parmIndex); + + // If body, then set sms body + Uri uri = Uri.parse(url); + String query = uri.getQuery(); + if (query != null) { + if (query.startsWith("body=")) { + intent.putExtra("sms_body", query.substring(5)); + } + } + } + intent.setData(Uri.parse("sms:" + address)); + intent.putExtra("address", address); + intent.setType("vnd.android-dir/mms-sms"); + cordova.getActivity().startActivity(intent); + override = true; + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); + } + } + // Test for whitelisted custom scheme names like mycoolapp:// or + // twitteroauthresponse:// (Twitter Oauth Response) + else if (!url.startsWith("http:") && !url.startsWith("https:") && url.matches("^[A-Za-z0-9+.-]*://.*?$")) { + if (allowedSchemes == null) { + String allowed = preferences.getString("AllowedSchemes", null); + if (allowed != null) { + allowedSchemes = allowed.split(","); + } + } + if (allowedSchemes != null) { + for (String scheme : allowedSchemes) { + if (url.startsWith(scheme)) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", "customscheme"); + obj.put("url", url); + sendUpdate(obj, true); + override = true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "Custom Scheme URI passed in has caused a JSON error."); + } + } + } + } + } + + if (useBeforeload) { + this.waitForBeforeload = true; + } + return override; + } + + private boolean sendBeforeLoad(String url, String method) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", BEFORELOAD); + obj.put("url", url); + if (method != null) { + obj.put("method", method); + } + sendUpdate(obj, true); + return true; + } catch (JSONException ex) { + LOG.e(LOG_TAG, "URI passed in has caused a JSON error."); + } + return false; + } + + /** + * New (added in API 21) For Android 5.0 and above. + * + * @param view + * @param request + */ + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return shouldInterceptRequest(request.getUrl().toString(), super.shouldInterceptRequest(view, request), + request.getMethod()); + } + + public WebResourceResponse shouldInterceptRequest(String url, WebResourceResponse response, String method) { + return response; + } + + /* + * onPageStarted fires the LOAD_START_EVENT + * + * @param view + * + * @param url + * + * @param favicon + */ + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + String newloc = ""; + if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) { + newloc = url; + } else { + // Assume that everything is HTTP at this point, because if we don't specify, + // it really should be. Complain loudly about this!!! + LOG.e(LOG_TAG, "Possible Uncaught/Unknown URI"); + newloc = "http://" + url; + } + + // Update the UI if we haven't already + if (!newloc.equals(edittext.getText().toString())) { + edittext.setText(newloc); + } + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_START_EVENT); + obj.put("url", newloc); + sendUpdate(obj, true); + } catch (JSONException ex) { + LOG.e(LOG_TAG, "URI passed in has caused a JSON error."); + } + } + + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + // Set the namespace for postMessage() + injectDeferredObject("window.webkit={messageHandlers:{cordova_iab:cordova_iab}}", null); + + // CB-10395 InAppBrowser's WebView not storing cookies reliable to local device + // storage + CookieManager.getInstance().flush(); + + // https://issues.apache.org/jira/browse/CB-11248 + view.clearFocus(); + view.requestFocus(); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_STOP_EVENT); + obj.put("url", url); + + sendUpdate(obj, true); + } catch (JSONException ex) { + LOG.d(LOG_TAG, "Should never happen"); + } + } + + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", failingUrl); + obj.put("code", errorCode); + obj.put("message", description); + + sendUpdate(obj, true, PluginResult.Status.ERROR); + } catch (JSONException ex) { + LOG.d(LOG_TAG, "Should never happen"); + } + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + super.onReceivedSslError(view, handler, error); + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", error.getUrl()); + obj.put("code", 0); + obj.put("sslerror", error.getPrimaryError()); + 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); + + sendUpdate(obj, true, PluginResult.Status.ERROR); + } catch (JSONException ex) { + LOG.d(LOG_TAG, "Should never happen"); + } + handler.cancel(); + } + + /** + * On received http auth request. + */ + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + + // Check if there is some plugin which can resolve this auth challenge + PluginManager pluginManager = null; + try { + Method gpm = webView.getClass().getMethod("getPluginManager"); + pluginManager = (PluginManager) gpm.invoke(webView); + } catch (NoSuchMethodException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (InvocationTargetException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + + if (pluginManager == null) { + try { + Field pmf = webView.getClass().getField("pluginManager"); + pluginManager = (PluginManager) pmf.get(webView); + } catch (NoSuchFieldException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } catch (IllegalAccessException e) { + LOG.d(LOG_TAG, e.getLocalizedMessage()); + } + } + + if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(webView, + new CordovaHttpAuthHandler(handler), host, realm)) { + return; + } + + // By default handle 401 like we'd normally do! + super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + } +} diff --git a/src/android/InAppBrowserDialog.java b/src/android/InAppBrowserDialog.java new file mode 100644 index 0000000..e7b212f --- /dev/null +++ b/src/android/InAppBrowserDialog.java @@ -0,0 +1,57 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.inappbrowser; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by Oliver on 22/11/2013. + */ +public class InAppBrowserDialog extends Dialog { + Context context; + InAppBrowser inAppBrowser = null; + + public InAppBrowserDialog(Context context, int theme) { + super(context, theme); + this.context = context; + } + + public void setInAppBroswer(InAppBrowser browser) { + this.inAppBrowser = browser; + } + + public void onBackPressed () { + if (this.inAppBrowser == null) { + this.dismiss(); + } else { + // better to go through the in inAppBrowser + // because it does a clean up + if (this.inAppBrowser.hardwareBack() && this.inAppBrowser.canGoBack()) { + this.inAppBrowser.goBack(); + } else { + this.inAppBrowser.closeDialog(); + } + } + } +} diff --git a/src/android/InAppChromeClient.java b/src/android/InAppChromeClient.java new file mode 100644 index 0000000..75a5a00 --- /dev/null +++ b/src/android/InAppChromeClient.java @@ -0,0 +1,186 @@ +/* + Copyright (c) 2021 BlackBerry Limited. All Rights Reserved. + Some modifications to the original cordova-plugin-inappbrowser + from https://github.com/apache/cordova-plugin-inappbrowser/ + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.inappbrowser; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +import android.os.Message; +import android.webkit.JsPromptResult; +import android.webkit.WebResourceRequest; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.webkit.GeolocationPermissions.Callback; + +import com.good.gd.cordova.core.webview.BBChromeClient; +import com.good.gd.cordova.core.webview.WebClientObserver; + +public class InAppChromeClient extends BBChromeClient { + + private CordovaWebView webView; + private String LOG_TAG = "InAppChromeClient"; + private long MAX_QUOTA = 100 * 1024 * 1024; + + public InAppChromeClient(WebClientObserver clientObserver, CordovaWebView webView) { + super(clientObserver); + this.webView = webView; + } + /** + * Handle database quota exceeded notification. + * + * @param url + * @param databaseIdentifier + * @param currentQuota + * @param estimatedSize + * @param totalUsedQuota + * @param quotaUpdater + */ + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) + { + LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); + quotaUpdater.updateQuota(MAX_QUOTA); + } + + /** + * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. + * + * @param origin + * @param callback + */ + @Override + public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { + super.onGeolocationPermissionsShowPrompt(origin, callback); + callback.invoke(origin, true, false); + } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * The prompt bridge provided for the InAppBrowser is capable of executing any + * oustanding callback belonging to the InAppBrowser plugin. Care has been + * taken that other callbacks cannot be triggered, and that no other code + * execution is possible. + * + * To trigger the bridge, the prompt default value should be of the form: + * + * gap-iab:// + * + * where is the string id of the callback to trigger (something + * like "InAppBrowser0123456789") + * + * If present, the prompt message is expected to be a JSON-encoded value to + * pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid. + * + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + */ + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + // See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute. + if (defaultValue != null && defaultValue.startsWith("gap")) { + if(defaultValue.startsWith("gap-iab://")) { + PluginResult scriptResult; + String scriptCallbackId = defaultValue.substring(10); + if (scriptCallbackId.matches("^InAppBrowser[0-9]{1,10}$")) { + if(message == null || message.length() == 0) { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray()); + } else { + try { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message)); + } catch(JSONException e) { + scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage()); + } + } + this.webView.sendPluginResult(scriptResult, scriptCallbackId); + result.confirm(""); + return true; + } + else { + // Anything else that doesn't look like InAppBrowser0123456789 should end up here + LOG.w(LOG_TAG, "InAppBrowser callback called with invalid callbackId : "+ scriptCallbackId); + result.cancel(); + return true; + } + } + else { + // Anything else with a gap: prefix should get this message + LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue); + result.cancel(); + return true; + } + } + return false; + } + + /** + * The InAppWebBrowser WebView is configured to MultipleWindow mode to mitigate a security + * bug found in Chromium prior to version 83.0.4103.106. + * See https://bugs.chromium.org/p/chromium/issues/detail?id=1083819 + * + * Valid Urls set to open in new window will be routed back to load in the original WebView. + * + * @param view + * @param isDialog + * @param isUserGesture + * @param resultMsg + * @return + */ + @Override + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + WebView inAppWebView = view; + final WebViewClient webViewClient = + new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + inAppWebView.loadUrl(request.getUrl().toString()); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + inAppWebView.loadUrl(url); + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } +} diff --git a/src/android/res/drawable-hdpi/ic_action_next_item.png b/src/android/res/drawable-hdpi/ic_action_next_item.png new file mode 100644 index 0000000000000000000000000000000000000000..fa469d8896a59934fbf36aebe69bbb8caec2a8b2 GIT binary patch literal 593 zcmV-X0@2ew#Qp~KZ0)f7AEsw$G6WMfh>{dU&BP{V_`c+kTrSs|-ZgLGzR9J{ zy{!4Zy}Qr-2m&uJFE6kEtgfvOjmP6j9LKw&D0Xco(6L}V!s7|;5^az6lBQ{S5*2{? zJ|53#L+(1GL{N!0RN~l10%617)4$)>`UYGP`4yn?j(__Huh&|rkN$JSH{vHTOn$>G zjroyY3G^Qj{}fLlnEZm-NSOyw0vcYoc)is^J==Kgx}pOoQH9rt zCcWqc6ufqLpQLJN3$IQS;3Tpdyk>}(q7&dGHZV&=sb@%H+hmOhgA-RP31AVM8io)l z0d{lI3BddV9yf;20_HQ(6@Ym%es2h^V16xn2=U%$Y6z`iekpnmuzAzAA&2>e=t-;` z@*~|m+FUe+*!;|JPNt3BD{u5fV{}r1%Xw2=I!}NuwrX&hyMje%$vhdqGo8?E;nG8b)mmC}IAq^-?d)0?>iAL56M&OH;S)f;MS6moTS;^pP# f<>ghSUjYUH07Aer2Yy7P00000NkvXXu0mjfn)D0> literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-hdpi/ic_action_previous_item.png b/src/android/res/drawable-hdpi/ic_action_previous_item.png new file mode 100644 index 0000000000000000000000000000000000000000..e861ecce9272c9c8192ff3935dc3445b3f8edd85 GIT binary patch literal 599 zcmV-d0;v6oP)t91EdOD4Sj+@^zv0XTmRam(r!l+aCYZn=AYlfBU}9$P%RX`G3S#ZobXi~09O)yy z_h@xNFv?|&XZIb8t!Dh@ekK#%6En0)S$V{i% zY%aFj?ZY9#DlC*E@g#g;pq^aCrI-PTZ=ZYgT*a&S z#TG=aPQ>d5{Cm&_O7VJ;d|zah(XowJpT*q75U)EvZiYPrrFdQNsg2^5YNUH4);{@5 laDBZV5fKp)@xSpUzyOzByV literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-hdpi/ic_action_remove.png b/src/android/res/drawable-hdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..f889617e4471d6a0c65dd81a060bea482276b322 GIT binary patch literal 438 zcmV;n0ZIOeP)uR$+`ZL}CL9iu4pv&~TPqL?R?{{9XWYPqIRgeD^;8#3vi0QmIrbmFkC- z=lNP%LWrBV*rEMwV3MsoHqsVpnjWjFdZ5h9V<&ATapw~E1PAb=v+=Jp6hzH@=Ksnh z?p@-Z`}Cg%jQ{`u07*qoM6N<$g0AVdX8-^I literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-mdpi/ic_action_next_item.png b/src/android/res/drawable-mdpi/ic_action_next_item.png new file mode 100644 index 0000000000000000000000000000000000000000..47365a300110db23c24d8471b4412cff3299335e GIT binary patch literal 427 zcmV;c0aX5pP)}4 zNQyzD&z|KhmJh*V!SWC28k$rgFbJCTOw9>SV0e#=m{d{d4v_RE6S|M4@9pf)?AB@( zD!dzlAQ+YTy?O!R7Jg<39}z}Tv@F9wqrkgk5hq4vW#Z%37t=J|d!9GIV_T>J#tHae zs|*lx#8HhW$b@f_0m;G{vp(Q)h-gX-!EN#1zjFg(;tztmoQbZOIJ;hgD^LMkhP9%& z3{#T~s7xf7HPhY~s)O+-_}IjNm^e^Wm8VZr!UUxt|dS5ABh@z}BtY}8#KEK=()eJQO_;pw;tFaVN+ VbL}20dqe;L002ovPDHLkV1lYyvRnWF literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-mdpi/ic_action_previous_item.png b/src/android/res/drawable-mdpi/ic_action_previous_item.png new file mode 100644 index 0000000000000000000000000000000000000000..4ad2df42755874f331733cfab5ad0931d5baf0a1 GIT binary patch literal 438 zcmV;n0ZIOeP)F6XU^oF|V`EEF^)OP= z@&q6Sa=>FC?mK(->^EZcF;UUyFyG4pu`{uHnW$j70gy^S5_$~u{bss20BAYLxADl9 zgMyzFGngo_To_0d0WlX;2o&cFiH&=T900T&WN9Xv<)DQ3m{haK4S`H(VuOKIN}9B4HXgw@Cj>q2Ew4lGVMbE8VbLGzP<|0lB`f{0u;Ljl>0<;2S6S19b_TMVkAc>0>#dN z97vaJ2rYgWA_>VtD=6BB08&W203|{o(*P>O59Bif#c$EAAcEEf$C26udk3UcBd1Y1 g3P!PbXFRCwBAU>F6XU=$26V8kbBY;0@^ zq{4u>@9f#Lk4Z5Ys8<$96#?-opx)!S)G-k-2V^-H$RMbJq*~4eHG_d%2lN5)H&Psd zX8AX$88`tmKFR0LpMR2)lDYX-@nh*(FEV>wX{AjT2oT23$* zkPyFQSx&M8a5#d2T+4}$0s_&&O0MO^WJ8(*W-3I%Z(_12RUCjL5gsSDoS?`7xUw3l zg%w#2Ae7H3aRf7^mQ(5oW@=bYi6aPQLy}84s2Y@7l#+5_6*+|rnj=YcL aAiw|+C&DJQP0oz~0000YQCYi7Ht0k1EPi|MH~WyXp>^jAd0~7hD?*o|7+zUSOmJ?LZ_^s!$gZ3u%7L@{I=7VP% z_a!2<(cVB@qEY}B;TUmjD@L`X^1Yi-;U)bXkac?RG$k<~ew|5Zb_7R-fV{idjgha>@ws{Ht;KCkj zQFSm8?6Kj>9;+mPy8VE^q9>%u-?;9XQRxD|Aiv-xvsxRx6znc_IpYTx zfWH#Qg58aMlJ_bA+ufNEGInWqK?Pv)KiS=j%2b%`t|x@t`LMen0`RADX~rk7pQv06 zWlo5c+MO@257h$(g5A+%H&VMCX1nVMc6aOB?z{=WIWqa27|$1Fc=Jme~K>w1^{LC0JpuQ^5_5n002ov JPDHLkV1f=sN^1ZB literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-xhdpi/ic_action_previous_item.png b/src/android/res/drawable-xhdpi/ic_action_previous_item.png new file mode 100644 index 0000000000000000000000000000000000000000..ed8ac91dec4b072dc40804c4b4178b065f7ed708 GIT binary patch literal 744 zcmVP)0=8N@8J`wu9@%$5uRVrBx8ftW!IF~j%BBb>Mv#M(Z}`;z8v z>q~mycWuAdYfB}CLZMJ76bgkxAqlNkt4qaoJ9|ROcW^o9p9_@XY&P4tXlq{}`3#qf z++S6c<1|elR;$%})B=Ef9ha+ustQU55vIEqArVHtZwa}_Fj()p-9!laGA@@WWkbjU z!sO<7JdXAU71YH0fFF zu}1R+4Vfqf$l2o(x2L9_;L{$1Y>txc{lXr-h!AXdnC+1!y(2?N!-qWv-5#e@*pefB z+2hp3xd0IYlKW(J^?H-V`$EC#{tFG)(%n=#3M_NLk z8A7Tq<^xe@gjj?kd!*svWUE}@34V@t9P5IwAq?PUsz((uHG(SFn?P~L$y-sCAB98M`&=jFYBD)$Dt=Dzu!75(wV*}TUW0~lD*P>x|&Qd7_P_ms8#d44)Bd2C{&(UWzlACh z2h+J7JT|oGD;)@GGOaOW6cZBo;L18>N<)*r-hvhul|7veGMpI|#*EiJJ~Wt%evs#k z`(T)NVD|L*hR@nt*!TS8@Q~!>{+{sRLemKr5utrj6SlHuxN0~$If#cv2)meV+a%%C zt)<5#^dMHnC&XAtwAM4=^N6;oJNO3DF++P6=euvz@Yh2KA?RyjT2C~1{B@8Z!OR?g=u^>1hj z_@^A1Ecxr>f}d?`p480di?4S%CphygyLXa^10xFBaPo&p0z-;&MeJ|;OL4%cX7F_N Kb6Mw<&;$S^P}b@I literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-xxhdpi/ic_action_next_item.png b/src/android/res/drawable-xxhdpi/ic_action_next_item.png new file mode 100644 index 0000000000000000000000000000000000000000..51479d8ddfdeccaf4af263b3420af6d536cab5ab GIT binary patch literal 1021 zcmVAgR7s3xqaChTCq{={$6$9pJ`w$`@*0PUS8yqfTW{_#D)Lv`px@2`t;Arl^M+khT`w(Vfi?T|Mb^%iv_Rq4U1O zfOIC@a@?e^XMrZPbBL#^h9X!@67>gteHBogO**6UmzLwHy0IW`R!G#{y&;LRXM`)O zCKMVkO2&U;;^`1IU_bGc#jS@ts9BAth4Sm0M#j^(r~&EY>6LWKA;S>dAMEJ74Gc(Y z!YU%3wk*Zds)dV`c)F&quOs5A2@|STsw%&fiHxT&EyUBp8A9UeJR+WsPy^D}glmqR zB5)Z;+T*D?1H5MhZiC zVZRAHMcoVw(*}4Z#Ezb-nPFkx05M@hqTWiMP)5xu8p6)P05M@56Hocj5Kpr)z>BA$ zGc1}Ko)HR{AEstl_fJn3par{*9*JP#6~f&!WE2MH{@} zPu)8K%QavW5jQ_;U2Rzyz554|S^pCr#L|i1kBOTavVJxONZbsae#o8e7()^1#BW8$ z&8eoiY1#mZo5Kk4$J*kiSpy_)mM!M?nOPBEy#d}H62}qZo7&t%dkyGE#?84iMxprb zA4J5>mG-!4$^eO*F+=ALh(D1adT`TZkjMa;^z5t5nq)7Ug8Ojn^Q}1Q?&vA zAU+Q*XR%|7mKtDcCPP*RSe(g_jR96?GUQYPc9Ld7%9&3*4{^}Rxw11#*&abG`bQE! z%e_N2>>0-kEH2&S>%BgZ_-mIbnzk(CMR#@g_wFbhH@~`!sc5?(ZxC)y;?Ep7hzT92 r000000000000000004ko_$|Nymn?!0<{Ma600000NkvXXu0mjfBNxJV literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-xxhdpi/ic_action_previous_item.png b/src/android/res/drawable-xxhdpi/ic_action_previous_item.png new file mode 100644 index 0000000000000000000000000000000000000000..bc8ff1241ade1ac7ad40427f4843b84fac947e62 GIT binary patch literal 1038 zcmV+p1o8WcP)mo zA`K~MkRSyq+|VX1EpADX!ubtpX-R2X{)3w|G(jRoT+p~M5_UmDVHa*Pqf8=#jI$FR zx|!vD(&-T1laBYiw{P!uE(8Do000000000000000fJA6En+=QS_547@@6xVYsZ?Gr zm&=)@b9;6G;5yEkAX@suJD;TiC?rQeVpkoe~${><9%@Z1CX%D6eP_#-l>|NT7R)*_qFEX2)Z z3?T74%D6eQ{ChHI;>$@{R;^sR=)*pDC&SwV2=Nay) zB5sa6#7!3lNZjlz4jFifo8}FWxY<|6P1oX^F+jPJff;*?o7Q_kH#l0!kdy(ka5gq> z&b`J>lLkoK>?q>qFeFBb3$tKAEHaSB4yM|O>Vm=-=W@XW7fwVD!p9biEK;}xjpZ62 zLt7q8q6g&I3m&*&f(PFoyy8#?wos z2`=KPn?h|dVXcU#JX@YvkEgCGOTo}PAv_Ple<6<}HMg*P=mGnQr>{fY zL;SMkIfCaWV#A=jh22IE$UmO)E$kCTGFlY*W|X81*f2p8PwUp=sr&Z@Nj$v`zaHkq z#M71}qk9;Tp9we0Ti7)#@idubDPC7tB=yQNiKi@XIik6V6&5Kj%o+tpD=d;TK#Dc^ z7Iw_t(7VWXxCH%2JpI=Si{I}Kn!|wo#nW9YEcg=jnZz&rnL*NT&fT%Xf(P9%R_e-< ze|Iz^H3&S|PExBc@MJ;*DgXcg00000000000001>2!0AM0Ol)zPaPAXwEzGB07*qo IM6N<$f+VNg^Z)<= literal 0 HcmV?d00001 diff --git a/src/android/res/drawable-xxhdpi/ic_action_remove.png b/src/android/res/drawable-xxhdpi/ic_action_remove.png new file mode 100644 index 0000000000000000000000000000000000000000..331c545b8cb07a97ee63cb4f1256d1dba5557a82 GIT binary patch literal 681 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n7ln*978H@y_sX^mF+0u=KlZOu|s+fxN=%TuH_vu-63%O z0bh(`mh@(>)(|r>v-Oq66AmapP&R+I|NHM}^O&8P#J-=iwAdNbrpXGl5g7zooZfe! zXy=~Q^508s)?R;|&#w7(p8l)j$+D}f&aa<&y-Vlf4wmJ z{m;$!KDWm<)IWJYp?`yVy?a)Rgy+7R7WpU3-7WlQo&Q_^@0!|=9dkD{|8ah%BK}}y zUwJzR`>Fc-=T0VbW}WtqPP|eM_FyzT{_JE!R zD)mzsyaca2=@)ZQY+Cdukl9DlG4$UH#$_D~bm}KDB)bJnw~uTvRI)fR-^3w}Mf~Sv z_7fcsRN|EmXe#eGIi0hhi=*$;Ql>v@O{@PLixhm)QT}Dxr&MO1o`9IAr*$3O_1veg z`?Qpqrz1f--tK_DQpAbv90e{c(m#7YGyP$o^P)53LpC?loVb0lEiVH0g|-+>zh76d zzw5&@{nHH0@veKsKFU^vF}CfCD89*-Dfr;V_F3ly55?}QZh7Ijuejxf>%Q!k7oPi) zTVD9?i*9)lxG%WnMd&{7mKTxxoLgSR?yG8<(OmPC!%+O^D#0YbPom04?mSuT+F^Mr zylcW%?L%qnpM8_#v%0PH_*W!j7~dy$w}_iMj&Eb;mhJQVTeVv6|AD73EMLFabmp+t zH{}!WBYrC$Dt@-J@Me4I8>JZ0$#0vg1>-jHzhy>_Y6gY|m493Y3}5QcuZR|#@EIiM M>FVdQ&MBb@07pA8tN;K2 literal 0 HcmV?d00001 diff --git a/src/ios/CDVInAppBrowserNavigationController.h b/src/ios/CDVInAppBrowserNavigationController.h new file mode 100644 index 0000000..bd186a2 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.h @@ -0,0 +1,27 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + + +@interface CDVInAppBrowserNavigationController : UINavigationController + +@property (nonatomic, weak) id orientationDelegate; + +@end diff --git a/src/ios/CDVInAppBrowserNavigationController.m b/src/ios/CDVInAppBrowserNavigationController.m new file mode 100644 index 0000000..3cc9043 --- /dev/null +++ b/src/ios/CDVInAppBrowserNavigationController.m @@ -0,0 +1,63 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserNavigationController.h" + +@implementation CDVInAppBrowserNavigationController : UINavigationController + +- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { + if ( self.presentedViewController) { + [super dismissViewControllerAnimated:flag completion:completion]; + } +} + +- (void) viewDidLoad { + [super viewDidLoad]; +} + +- (CGRect) invertFrameIfNeeded:(CGRect)rect { + if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])) { + CGFloat temp = rect.size.width; + rect.size.width = rect.size.height; + rect.size.height = temp; + } + rect.origin = CGPointZero; + return rect; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +@end diff --git a/src/ios/CDVInAppBrowserOptions.h b/src/ios/CDVInAppBrowserOptions.h new file mode 100644 index 0000000..c1c9fa5 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.h @@ -0,0 +1,50 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + + +@interface CDVInAppBrowserOptions : NSObject {} + +@property (nonatomic, assign) BOOL location; +@property (nonatomic, assign) BOOL toolbar; +@property (nonatomic, copy) NSString* closebuttoncaption; +@property (nonatomic, copy) NSString* closebuttoncolor; +@property (nonatomic, assign) BOOL lefttoright; +@property (nonatomic, copy) NSString* toolbarposition; +@property (nonatomic, copy) NSString* toolbarcolor; +@property (nonatomic, assign) BOOL toolbartranslucent; +@property (nonatomic, assign) BOOL hidenavigationbuttons; +@property (nonatomic, copy) NSString* navigationbuttoncolor; +@property (nonatomic, assign) BOOL cleardata; +@property (nonatomic, assign) BOOL clearcache; +@property (nonatomic, assign) BOOL clearsessioncache; +@property (nonatomic, assign) BOOL hidespinner; + +@property (nonatomic, copy) NSString* presentationstyle; +@property (nonatomic, copy) NSString* transitionstyle; + +@property (nonatomic, assign) BOOL enableviewportscale; +@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; +@property (nonatomic, assign) BOOL allowinlinemediaplayback; +@property (nonatomic, assign) BOOL hidden; +@property (nonatomic, assign) BOOL disallowoverscroll; +@property (nonatomic, copy) NSString* beforeload; + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; + +@end diff --git a/src/ios/CDVInAppBrowserOptions.m b/src/ios/CDVInAppBrowserOptions.m new file mode 100644 index 0000000..e20d1a8 --- /dev/null +++ b/src/ios/CDVInAppBrowserOptions.m @@ -0,0 +1,90 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVInAppBrowserOptions.h" + +@implementation CDVInAppBrowserOptions + +- (id)init +{ + if (self = [super init]) { + // default values + self.location = YES; + self.toolbar = YES; + self.closebuttoncaption = nil; + self.toolbarposition = @"bottom"; + self.cleardata = NO; + self.clearcache = NO; + self.clearsessioncache = NO; + self.hidespinner = NO; + + self.enableviewportscale = NO; + self.mediaplaybackrequiresuseraction = NO; + self.allowinlinemediaplayback = NO; + self.hidden = NO; + self.disallowoverscroll = NO; + self.hidenavigationbuttons = NO; + self.closebuttoncolor = nil; + self.lefttoright = false; + self.toolbarcolor = nil; + self.toolbartranslucent = YES; + self.beforeload = @""; + } + + return self; +} + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options +{ + CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; + + // NOTE: this parsing does not handle quotes within values + NSArray* pairs = [options componentsSeparatedByString:@","]; + + // parse keys and values, set the properties + for (NSString* pair in pairs) { + NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; + + if ([keyvalue count] == 2) { + NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; + NSString* value = [keyvalue objectAtIndex:1]; + NSString* value_lc = [value lowercaseString]; + + BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setAllowsFloats:YES]; + BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; + + // set the property according to the key name + if ([obj respondsToSelector:NSSelectorFromString(key)]) { + if (isNumber) { + [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; + } else if (isBoolean) { + [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; + } else { + [obj setValue:value forKey:key]; + } + } + } + } + + return obj; +} + +@end diff --git a/src/ios/CDVWKInAppBrowser.h b/src/ios/CDVWKInAppBrowser.h new file mode 100644 index 0000000..2f650b8 --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.h @@ -0,0 +1,80 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import +#import +#import "CDVWKInAppBrowserUIDelegate.h" +#import "CDVInAppBrowserOptions.h" +#import "CDVInAppBrowserNavigationController.h" + +@class CDVWKInAppBrowserViewController; + +@interface CDVWKInAppBrowser : CDVPlugin { + UIWindow * tmpWindow; + + @private + NSString* _beforeload; + BOOL _waitForBeforeload; +} + +@property (nonatomic, retain) CDVWKInAppBrowser* instance; +@property (nonatomic, retain) CDVWKInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + ++ (id) getInstance; +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; +- (void)hide:(CDVInvokedUrlCommand*)command; +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVWKInAppBrowserViewController : UIViewController { + @private + CDVInAppBrowserOptions *_browserOptions; + NSDictionary *_settings; +} + +@property (nonatomic, strong) IBOutlet WKWebView* webView; +@property (nonatomic, strong) IBOutlet WKWebViewConfiguration* configuration; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; +@property (nonatomic, strong) IBOutlet CDVWKInAppBrowserUIDelegate* webViewUIDelegate; + +@property (nonatomic, weak) id orientationDelegate; +@property (nonatomic, weak) CDVWKInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString : (int) buttonIndex; + +- (id)initWithBrowserOptions: (CDVInAppBrowserOptions*) browserOptions andSettings:(NSDictionary*) settings; + +@end diff --git a/src/ios/CDVWKInAppBrowser.m b/src/ios/CDVWKInAppBrowser.m new file mode 100644 index 0000000..9509b87 --- /dev/null +++ b/src/ios/CDVWKInAppBrowser.m @@ -0,0 +1,1380 @@ +/* + Copyright (c) 2021 BlackBerry Limited. All Rights Reserved. + Some modifications to the original cordova-plugin-inappbrowser + from https://github.com/apache/cordova-plugin-inappbrowser/ + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowser.h" +#import + +#if __has_include() // Cordova-iOS >=6 + #import +#elif __has_include("CDVWKProcessPoolFactory.h") // Cordova-iOS <6 with WKWebView plugin + #import "CDVWKProcessPoolFactory.h" +#endif + +#import + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define IAB_BRIDGE_NAME @"cordova_iab" + +#define TOOLBAR_HEIGHT 44.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +static NSURLCredential *acceptedCertificateCredential; +static NSError *serverTrustError; + +#pragma mark CDVWKInAppBrowser + +@interface CDVWKInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVWKInAppBrowser + +static CDVWKInAppBrowser* instance = nil; + ++ (id) getInstance{ + return instance; +} + +- (void)pluginInitialize +{ + instance = self; + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; + _beforeload = @""; + _waitForBeforeload = NO; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { + NSURL* baseUrl = [self.webViewEngine URL]; + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + if (browserOptions.cleardata) { + + NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; + [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:dateFrom completionHandler:^{ + NSLog(@"Removed all WKWebView data"); + self.inAppBrowserViewController.webView.configuration.processPool = [[WKProcessPool alloc] init]; // create new process pool to flush all data + }]; + } + + if (browserOptions.clearcache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + + if(isAtLeastiOS11){ +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // Deletes all cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + }]; +#endif + }else{ + // https://stackoverflow.com/a/31803708/777265 + // Only deletes domain cookies (not session cookies) + [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] + completionHandler:^(NSArray * __nonnull records) { + for (WKWebsiteDataRecord *record in records){ + NSSet* dataTypes = record.dataTypes; + if([dataTypes containsObject:WKWebsiteDataTypeCookies]){ + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes + forDataRecords:@[record] + completionHandler:^{}]; + } + } + }]; + } + } + + if (browserOptions.clearsessioncache) { + bool isAtLeastiOS11 = false; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + isAtLeastiOS11 = true; + } +#endif + if (isAtLeastiOS11) { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // Deletes session cookies + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + [cookieStore getAllCookies:^(NSArray* cookies) { + NSHTTPCookie* cookie; + for(cookie in cookies){ + if(cookie.sessionOnly){ + [cookieStore deleteCookie:cookie completionHandler:nil]; + } + } + }]; +#endif + }else{ + NSLog(@"clearsessioncache not available below iOS 11.0"); + } + } + + if (self.inAppBrowserViewController == nil) { + self.inAppBrowserViewController = [[CDVWKInAppBrowserViewController alloc] initWithBrowserOptions: browserOptions andSettings:self.commandDelegate.settings]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil || browserOptions.closebuttoncolor != nil) { + int closeButtonIndex = browserOptions.lefttoright ? (browserOptions.hidenavigationbuttons ? 1 : 4) : 0; + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption :browserOptions.closebuttoncolor :closeButtonIndex]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + //prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + // use of beforeload event + if([browserOptions.beforeload isKindOfClass:[NSString class]]){ + _beforeload = browserOptions.beforeload; + }else{ + _beforeload = @"yes"; + } + _waitForBeforeload = ![_beforeload isEqualToString:@""]; + + [self.inAppBrowserViewController navigateTo:url]; + if (!browserOptions.hidden) { + [self show:nil withNoAnimate:browserOptions.hidden]; + } +} + +- (void)show:(CDVInvokedUrlCommand*)command{ + [self show:command withNoAnimate:NO]; +} + +- (void)show:(CDVInvokedUrlCommand*)command withNoAnimate:(BOOL)noAnimate +{ + BOOL initHidden = NO; + if(command == nil && noAnimate == YES){ + initHidden = YES; + } + + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + if(!initHidden){ + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + } + + __block CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + nav.modalPresentationStyle = self.inAppBrowserViewController.modalPresentationStyle; + nav.presentationController.delegate = self.inAppBrowserViewController; + + __weak CDVWKInAppBrowser* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.inAppBrowserViewController != nil) { + float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf->tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + if(initHidden && osVersion < 11){ + frame.origin.x = -10000; + } + strongSelf->tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } + UIViewController *tmpController = [[UIViewController alloc] init]; + [strongSelf->tmpWindow setRootViewController:tmpController]; + [strongSelf->tmpWindow setWindowLevel:UIWindowLevelNormal]; + + if(!initHidden || osVersion < 11){ + [self->tmpWindow makeKeyAndVisible]; + } + [tmpController presentViewController:nav animated:!noAnimate completion:nil]; + } + }); +} + +- (void)hide:(CDVInvokedUrlCommand*)command +{ + // Set tmpWindow to hidden to make main webview responsive to touch again + // https://stackoverflow.com/questions/4544489/how-to-remove-a-uiwindow + self->tmpWindow.hidden = YES; + self->tmpWindow = nil; + + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to hide IAB after it was closed."); + return; + + + } + if (_previousStatusBarStyle == -1) { + NSLog(@"Tried to hide IAB while already hidden"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + _previousStatusBarStyle = -1; + [self.inAppBrowserViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + // the webview engine itself will filter for this according to policy + // in config.xml for cordova-ios-4.0 + [self.webViewEngine loadRequest:request]; +} + +- (void)openInSystem:(NSURL*)url +{ + if ([[UIApplication sharedApplication] openURL:url] == NO) { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + [[UIApplication sharedApplication] openURL:url]; + } +} + +- (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command +{ + NSString* urlStr = [command argumentAtIndex:0]; + + if ([_beforeload isEqualToString:@""]) { + NSLog(@"unexpected loadAfterBeforeload called without feature beforeload=get|post"); + } + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to invoke loadAfterBeforeload on IAB after it was closed."); + return; + } + if (urlStr == nil) { + NSLog(@"loadAfterBeforeload called with nil argument, ignoring."); + return; + } + + NSURL* url = [NSURL URLWithString:urlStr]; + //_beforeload = @""; + _waitForBeforeload = NO; + [self.inAppBrowserViewController navigateTo:url]; +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + // Ensure a message handler bridge is created to communicate with the CDVWKInAppBrowserViewController + [self evaluateJavaScript: [NSString stringWithFormat:@"(function(w){if(!w._cdvMessageHandler) {w._cdvMessageHandler = function(id,d){w.webkit.messageHandlers.%@.postMessage({d:d, id:id});}}})(window)", IAB_BRIDGE_NAME]]; + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self evaluateJavaScript:jsToInject]; + } + } else { + [self evaluateJavaScript:source]; + } +} + + +//Synchronus helper for javascript evaluation +- (void)evaluateJavaScript:(NSString *)script { + __block NSString* _script = script; + [self.inAppBrowserViewController.webView evaluateJavaScript:script completionHandler:^(id result, NSError *error) { + if (error == nil) { + if (result != nil) { + NSLog(@"%@", result); + } + } else { + NSLog(@"evaluateJavaScript error : %@ : %@", error.localizedDescription, _script); + } + }]; +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvMessageHandler('%@',JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvMessageHandler('%@'); }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The message handler bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + */ +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + + NSURL* url = navigationAction.request.URL; + NSURL* mainDocumentURL = navigationAction.request.mainDocumentURL; + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + BOOL shouldStart = YES; + BOOL useBeforeLoad = NO; + NSString* httpMethod = navigationAction.request.HTTPMethod; + NSString* errorMessage = nil; + + if([_beforeload isEqualToString:@"post"]){ + //TODO handle POST requests by preserving POST data then remove this condition + errorMessage = @"beforeload doesn't yet support POST requests"; + } + else if(isTopLevelNavigation && ( + [_beforeload isEqualToString:@"yes"] + || ([_beforeload isEqualToString:@"get"] && [httpMethod isEqualToString:@"GET"]) + // TODO comment in when POST requests are handled + // || ([_beforeload isEqualToString:@"post"] && [httpMethod isEqualToString:@"POST"]) + )){ + useBeforeLoad = YES; + } + + // When beforeload, on first URL change, initiate JS callback. Only after the beforeload event, continue. + if (_waitForBeforeload && useBeforeLoad) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"beforeload", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + if(errorMessage != nil){ + NSLog(errorMessage); + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":[url absoluteString], @"code": @"-1", @"message": errorMessage}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + //if is an app store link, let the system handle it, otherwise it fails to load it + if ([[ url scheme] isEqualToString:@"itms-appss"] || [[ url scheme] isEqualToString:@"itms-apps"]) { + [theWebView stopLoading]; + [self openInSystem:url]; + shouldStart = NO; + } + else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + if (useBeforeLoad) { + _waitForBeforeload = YES; + } + + if(shouldStart){ + // Fix GH-417 & GH-424: Handle non-default target attribute + // Based on https://stackoverflow.com/a/25713070/777265 + if (!navigationAction.targetFrame){ + [theWebView loadRequest:navigationAction.request]; + decisionHandler(WKNavigationActionPolicyCancel); + }else{ + decisionHandler(WKNavigationActionPolicyAllow); + } + }else{ + decisionHandler(WKNavigationActionPolicyCancel); + } +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + + CDVPluginResult* pluginResult = nil; + + if([message.body isKindOfClass:[NSDictionary class]]){ + NSDictionary* messageContent = (NSDictionary*) message.body; + NSString* scriptCallbackId = messageContent[@"id"]; + + if([messageContent objectForKey:@"d"]){ + NSString* scriptResult = messageContent[@"d"]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + }else if(self.callbackId != nil){ + // Send a message event + NSString* messageContent = (NSString*) message.body; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[messageContent dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if (error == nil) { + NSMutableDictionary* dResult = [NSMutableDictionary new]; + [dResult setValue:@"message" forKey:@"type"]; + [dResult setObject:decodedResult forKey:@"data"]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dResult]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + } +} + +- (void)didStartProvisionalNavigation:(WKWebView*)theWebView +{ + NSLog(@"didStartProvisionalNavigation"); +// self.inAppBrowserViewController.currentURL = theWebView.URL; +} + +- (void)didFinishNavigation:(WKWebView*)theWebView +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.inAppBrowserViewController.currentURL != nil){ + url = [self.inAppBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + + [self.inAppBrowserViewController.configuration.userContentController removeScriptMessageHandlerForName:IAB_BRIDGE_NAME]; + self.inAppBrowserViewController.configuration = nil; + + [self.inAppBrowserViewController.webView stopLoading]; + [self.inAppBrowserViewController.webView removeFromSuperview]; + [self.inAppBrowserViewController.webView setUIDelegate:nil]; + [self.inAppBrowserViewController.webView setNavigationDelegate:nil]; + self.inAppBrowserViewController.webView = nil; + + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + self.inAppBrowserViewController = nil; + + // Set tmpWindow to hidden to make main webview responsive to touch again + // Based on https://stackoverflow.com/questions/4544489/how-to-remove-a-uiwindow + self->tmpWindow.hidden = YES; + self->tmpWindow = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + if (_previousStatusBarStyle != -1) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + + } + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end //CDVWKInAppBrowser + +#pragma mark CDVWKInAppBrowserViewController + +@implementation CDVWKInAppBrowserViewController + +@synthesize currentURL; + +CGFloat lastReducedStatusBarHeight = 0.0; +BOOL isExiting = FALSE; + +- (id)initWithBrowserOptions: (CDVInAppBrowserOptions*) browserOptions andSettings:(NSDictionary *)settings +{ + self = [super init]; + if (self != nil) { + _browserOptions = browserOptions; + _settings = settings; + self.webViewUIDelegate = [[CDVWKInAppBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; + [self.webViewUIDelegate setViewController:self]; + + [self createViews]; + } + + return self; +} + +-(void)dealloc { + //NSLog(@"dealloc"); +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + + NSString *userAgent = configuration.applicationNameForUserAgent; + if ( + [self settingForKey:@"OverrideUserAgent"] == nil && + [self settingForKey:@"AppendUserAgent"] != nil + ) { + userAgent = [NSString stringWithFormat:@"%@ %@", userAgent, [self settingForKey:@"AppendUserAgent"]]; + } + configuration.applicationNameForUserAgent = userAgent; + configuration.userContentController = userContentController; +#if __has_include() + configuration.processPool = [[CDVWebViewProcessPoolFactory sharedFactory] sharedProcessPool]; +#elif __has_include("CDVWKProcessPoolFactory.h") + configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool]; +#endif + [configuration.userContentController addScriptMessageHandler:self name:IAB_BRIDGE_NAME]; + + //WKWebView options + configuration.allowsInlineMediaPlayback = _browserOptions.allowinlinemediaplayback; + if (IsAtLeastiOSVersion(@"10.0")) { + configuration.ignoresViewportScaleLimits = _browserOptions.enableviewportscale; + if(_browserOptions.mediaplaybackrequiresuseraction == YES){ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + }else{ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } + }else{ // iOS 9 + configuration.mediaPlaybackRequiresUserAction = _browserOptions.mediaplaybackrequiresuseraction; + } + + if (@available(iOS 13.0, *)) { + NSString *contentMode = [self settingForKey:@"PreferredContentMode"]; + if ([contentMode isEqual: @"mobile"]) { + configuration.defaultWebpagePreferences.preferredContentMode = WKContentModeMobile; + } else if ([contentMode isEqual: @"desktop"]) { + configuration.defaultWebpagePreferences.preferredContentMode = WKContentModeDesktop; + } + + } + + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + + self.webView.navigationDelegate = self; + self.webView.UIDelegate = self.webViewUIDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + if ([self settingForKey:@"OverrideUserAgent"] != nil) { + self.webView.customUserAgent = [self settingForKey:@"OverrideUserAgent"]; + } + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.userInteractionEnabled = YES; + self.automaticallyAdjustsScrollViewInsets = YES ; + [self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; + self.webView.allowsLinkPreview = NO; + self.webView.allowsBackForwardNavigationGestures = NO; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (@available(iOS 11.0, *)) { + [self.webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; + } +#endif + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin); + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(CGRectGetMidX(self.webView.frame), CGRectGetMidY(self.webView.frame), 20.0, 20.0); + self.spinner.hidden = NO; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + if (_browserOptions.toolbarcolor != nil) { // Set toolbar color if user sets it in options + self.toolbar.barTintColor = [self colorFromHexString:_browserOptions.toolbarcolor]; + } + if (!_browserOptions.toolbartranslucent) { // Set toolbar translucent to no if user sets it in options + self.toolbar.translucent = NO; + } + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor blackColor]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.forwardButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + if (_browserOptions.navigationbuttoncolor != nil) { // Set button color if user sets it in options + self.backButton.tintColor = [self colorFromHexString:_browserOptions.navigationbuttoncolor]; + } + + // Filter out Navigation Buttons if user requests so + if (_browserOptions.hidenavigationbuttons) { + if (_browserOptions.lefttoright) { + [self.toolbar setItems:@[flexibleSpaceButton, self.closeButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton]]; + } + } else if (_browserOptions.lefttoright) { + [self.toolbar setItems:@[self.backButton, fixedSpaceButton, self.forwardButton, flexibleSpaceButton, self.closeButton]]; + } else { + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + } + + self.view.backgroundColor = [UIColor clearColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (id)settingForKey:(NSString*)key +{ + return [_settings objectForKey:[key lowercaseString]]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title : (NSString*) colorString : (int) buttonIndex +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + // Initialize with title if title is set, otherwise the title will be 'Done' localized + self.closeButton = title != nil ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)] : [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + // If color on closebutton is requested then initialize with that that color, otherwise use initialize with default + self.closeButton.tintColor = colorString != nil ? [self colorFromHexString:colorString] : [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:buttonIndex withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + if (isExiting && (self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + isExiting = FALSE; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + NSString* statusBarStylePreference = [self settingForKey:@"InAppBrowserStatusBarStyle"]; + if (statusBarStylePreference && [statusBarStylePreference isEqualToString:@"lightcontent"]) { + return UIStatusBarStyleLightContent; + } else if (statusBarStylePreference && [statusBarStylePreference isEqualToString:@"darkcontent"]) { + if (@available(iOS 13.0, *)) { + return UIStatusBarStyleDarkContent; + } else { + return UIStatusBarStyleDefault; + } + } else { + return UIStatusBarStyleDefault; + } +} + +- (BOOL)prefersStatusBarHidden { + return NO; +} + +- (void)close +{ + self.currentURL = nil; + + __weak UIViewController* weakSelf = self; + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + isExiting = TRUE; + lastReducedStatusBarHeight = 0.0; + if ([weakSelf respondsToSelector:@selector(presentingViewController)]) { + [[weakSelf presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[weakSelf parentViewController] dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + if ([url.scheme isEqualToString:@"file"]) { + [self.webView loadFileURL:url allowingReadAccessToURL:url]; + } else { + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + [self.webView loadRequest:request]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// 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. +// +- (float) getStatusBarOffset { + return (float) IsAtLeastiOSVersion(@"7.0") ? [[UIApplication sharedApplication] statusBarFrame].size.height : 0.0; +} + +- (void) rePositionViews { + CGRect viewBounds = [self.webView bounds]; + CGFloat statusBarHeight = [self getStatusBarOffset]; + + // orientation portrait or portraitUpsideDown: status bar is on the top and web view is to be aligned to the bottom of the status bar + // orientation landscapeLeft or landscapeRight: status bar height is 0 in but lets account for it in case things ever change in the future + viewBounds.origin.y = statusBarHeight; + + // account for web view height portion that may have been reduced by a previous call to this method + viewBounds.size.height = viewBounds.size.height - statusBarHeight + lastReducedStatusBarHeight; + lastReducedStatusBarHeight = statusBarHeight; + + if ((_browserOptions.toolbar) && ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop])) { + // if we have to display the toolbar on top of the web view, we need to account for its height + viewBounds.origin.y += TOOLBAR_HEIGHT; + self.toolbar.frame = CGRectMake(self.toolbar.frame.origin.x, statusBarHeight, self.toolbar.frame.size.width, self.toolbar.frame.size.height); + } + + self.webView.frame = viewBounds; +} + +// 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 +- (UIColor *)colorFromHexString:(NSString *)hexString { + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; +} + +#pragma mark WKNavigationDelegate + +- (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigation *)navigation{ + + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + NSLog(_browserOptions.hidespinner ? @"Yes" : @"No"); + if(!_browserOptions.hidespinner) { + [self.spinner startAnimating]; + } + + return [self.navigationDelegate didStartProvisionalNavigation:theWebView]; +} + +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSURL *url = navigationAction.request.URL; + NSURL *mainDocumentURL = navigationAction.request.mainDocumentURL; + + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + + if (isTopLevelNavigation) { + self.currentURL = url; + } + + [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; +} + +- (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navigation +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + theWebView.scrollView.contentInset = UIEdgeInsetsZero; + + [self.spinner stopAnimating]; + + [self.navigationDelegate didFinishNavigation:theWebView]; +} + +- (void)webView:(WKWebView*)theWebView failedNavigation:(NSString*) delegateName withError:(nonnull NSError *)error{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:%@ - %ld: %@", delegateName, (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailNavigation:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailNavigation" withError:error]; +} + +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + if (![message.name isEqualToString:IAB_BRIDGE_NAME]) { + return; + } + //NSLog(@"Received script message %@", message.body); + [self.navigationDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [coordinator animateAlongsideTransition:^(id context) + { + [self rePositionViews]; + } completion:^(id context) + { + + }]; + + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; +} + +#pragma mark UIAdaptivePresentationControllerDelegate + +- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController { + isExiting = TRUE; +} + +#pragma mark Server trust section + +- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { + + NSString* authenticationMethod = challenge.protectionSpace.authenticationMethod; + + if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) + { + completionHandler(NSURLSessionAuthChallengeUseCredential, acceptedCertificateCredential); + return; + } + + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + if (error.code == NSURLErrorServerCertificateUntrusted) + { + [self displayServerTrustDialogFromError:error]; + } + + [self webView:theWebView failedNavigation:@"didFailProvisionalNavigation" withError:error]; +} + +- (void)displayServerTrustDialogFromError:(NSError *)error +{ + serverTrustError = error; + + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:error.localizedDescription + message:error.localizedRecoverySuggestion + preferredStyle:UIAlertControllerStyleAlert]; + + __weak __typeof__(alertController) weakAlertController = alertController; + __weak __typeof__(self) weakSelf = self; + + [alertController addAction:[UIAlertAction actionWithTitle:@"Deny" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * _Nonnull action) { + [weakSelf showUntrustedResourceError:error]; + [weakAlertController + dismissViewControllerAnimated:YES + completion:nil]; + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:@"Always trust" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + [weakSelf trustServerCertificate]; + [weakSelf proceedLoadingRequest]; + [weakAlertController + dismissViewControllerAnimated:YES + completion:nil]; + }]]; + + [self presentViewController:alertController animated:YES completion:nil]; + }); +} + +- (void)showUntrustedResourceError:(NSError *)error +{ + [self.spinner stopAnimating]; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.addressLabel.text = NSLocalizedString([error localizedDescription], nil); +} + +- (void)trustServerCertificate +{ + SecTrustRef trust = (__bridge SecTrustRef)([serverTrustError.userInfo objectForKey:NSURLErrorFailingURLPeerTrustErrorKey]); + CFDataRef exceptions = SecTrustCopyExceptions (trust); + SecTrustSetExceptions (trust, exceptions); + CFRelease (exceptions); + + acceptedCertificateCredential = [NSURLCredential credentialForTrust:trust]; +#if !defined (DISABLE_GD) + acceptedCertificateCredential.gdPersistence = NSURLCredentialPersistencePermanent; +#endif +} + +- (void)proceedLoadingRequest +{ + NSURLRequest* request = [NSURLRequest requestWithURL:[[self currentURL] absoluteURL]]; + [self.webView loadRequest:request]; +} + +@end //CDVWKInAppBrowserViewController diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.h b/src/ios/CDVWKInAppBrowserUIDelegate.h new file mode 100644 index 0000000..1a6ea22 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.h @@ -0,0 +1,32 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import + +@interface CDVWKInAppBrowserUIDelegate : NSObject { + @private + UIViewController* _viewController; +} + +@property (nonatomic, copy) NSString* title; + +- (instancetype)initWithTitle:(NSString*)title; +-(void) setViewController:(UIViewController*) viewController; + +@end diff --git a/src/ios/CDVWKInAppBrowserUIDelegate.m b/src/ios/CDVWKInAppBrowserUIDelegate.m new file mode 100644 index 0000000..4bc7a76 --- /dev/null +++ b/src/ios/CDVWKInAppBrowserUIDelegate.m @@ -0,0 +1,127 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWKInAppBrowserUIDelegate.h" + +@implementation CDVWKInAppBrowserUIDelegate + +- (instancetype)initWithTitle:(NSString*)title +{ + self = [super init]; + if (self) { + self.title = title; + } + + return self; +} + +- (void) webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(void))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message + initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(YES); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(NO); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +- (void) webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt + defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame + completionHandler:(void (^)(NSString* result))completionHandler +{ + UIAlertController* alert = [UIAlertController alertControllerWithTitle:self.title + message:prompt + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(((UITextField*)alert.textFields[0]).text); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + + [alert addAction:ok]; + + UIAlertAction* cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction* action) + { + completionHandler(nil); + [alert dismissViewControllerAnimated:YES completion:nil]; + }]; + [alert addAction:cancel]; + + [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) { + textField.text = defaultText; + }]; + + [[self getViewController] presentViewController:alert animated:YES completion:nil]; +} + +-(UIViewController*) getViewController +{ + return _viewController; +} + +-(void) setViewController:(UIViewController*) viewController +{ + _viewController = viewController; +} + +@end diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml new file mode 100644 index 0000000..6afba65 --- /dev/null +++ b/tests/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + jasmine: true \ No newline at end of file diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..729e6c0 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,14 @@ +{ + "name": "cordova-plugin-inappbrowser-tests", + "version": "5.0.0", + "description": "", + "cordova": { + "id": "cordova-plugin-inappbrowser-tests", + "platforms": [] + }, + "keywords": [ + "ecosystem:cordova" + ], + "author": "", + "license": "Apache-2.0" +} diff --git a/tests/plugin.xml b/tests/plugin.xml new file mode 100644 index 0000000..e250ad7 --- /dev/null +++ b/tests/plugin.xml @@ -0,0 +1,33 @@ + + + + + Cordova InAppBrowser Plugin Tests + Apache 2.0 + + + + + + + + diff --git a/tests/resources/inject.css b/tests/resources/inject.css new file mode 100644 index 0000000..3f6e41c --- /dev/null +++ b/tests/resources/inject.css @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. +*/ +#style-update-file { + display: block !important; +} diff --git a/tests/resources/inject.html b/tests/resources/inject.html new file mode 100644 index 0000000..3004b35 --- /dev/null +++ b/tests/resources/inject.html @@ -0,0 +1,44 @@ + + + + + + + + + Cordova Mobile Spec + + + +

InAppBrowser - Script / Style Injection Test

+ + +
User-Agent:
+ + + diff --git a/tests/resources/inject.js b/tests/resources/inject.js new file mode 100644 index 0000000..bf94d4e --- /dev/null +++ b/tests/resources/inject.js @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var d = document.getElementById('header'); +d.innerHTML = 'Script file successfully injected'; diff --git a/tests/resources/local.html b/tests/resources/local.html new file mode 100644 index 0000000..d23a714 --- /dev/null +++ b/tests/resources/local.html @@ -0,0 +1,67 @@ + + + + + + + + + IAB test page + + + + + +

Local URL

+
+ You have successfully loaded a local URL: + +
+
+
User-Agent =
+
+
Likely running inAppBrowser: Device version from Cordova=not found, Back link should not work, toolbar may be present, logcat should show failed 'gap:' calls.
+
+
Visit Google (whitelisted)
+
Visit Yahoo (not whitelisted)
+ + +

Back +

+ +

tall div with border
+ + + diff --git a/tests/resources/local.pdf b/tests/resources/local.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b54f1b759a9ab7c60b05b209a84e1eb2cabe455a GIT binary patch literal 8568 zcmb_?1yqz>7p_RjptJ}|3?(Qr1v*Vp-ziV%JZYn4VLWE!dp61z&*`=J#ryrYJ z0Kz~p(Am-sASnq1X`q~J@U}n^0;CHBDPbJ&C@kUA0f|Q`psbv&Q9x;F01l5uAsqpp zM2e4Or~|+>?>wa6U0JlK%;#Kr;p)nTnX+Kf2%@<8a$JV3G2+wVSbEJ2`gLLtLU=~- z2RdSO?EVm$JLedyP~uHi7#E5L8zXLPuJ^RTl1B7yT&;wMe{4C+Ae?b)_p zi2db=KcfdZ(T1R&77zji$)1GJi4Z>^NCD-Ju|g?hkzW5hv_S@%4p$6l*lY%^0bKpI7llFHGF&#JHbwfK>d&N}gK(b23c9rRyl|o1S z3zq3G=O&Iyx*nb3BFi#*^{C|Tu1@^@7KTn{bEB7Y;SCk)mA)N_+h@lB2RUn68>WwF z@=TtaPE=2ruWUBab%yw>KQx`Gj*6%fxX;%r)sP<8zu04Ry#FyBZL+iJ{7zh$>^xg< zQ)wpKoy(q0h1ysL?K2?eBNZYhzux&Q*xMRNosQ^6*NR@l-JXbR_qi+abmLdWwx~BQ zv}Gs-N^ZCsHjZVV6=o4gf7y4ae_!L~Mh(hF=OQo1GRsRcX8rJ&Ps~W-is^-0{aom| zcD#ANBwxYfKDye{N-Ovrj~aCwl;>6Laewx6lk?SL)eTY21?7_W&G$<_fBW^QttDjT zY)!#@BYZ4qAUkIuH!X60A~N_*Vdoe^&3wpWZx z#|)($u|B2}`?EU=ftCifo(JgeH>G%Ps<)-NU>}) zi#^O)z){f!V6&8@K2x?H{K3y@Jd3r@-R_Z{*9vmQ)lqJ#e+NVU+VKnUv%lzzm6rAo z?&E{5B3@-NQuaL#&FIn?7K4iid!fAzHc~=qdJ$-va>X1jvVdGFD)hwhrw&|DrAlqU z*qc7num0kU-8zP&A*z?x67x9HUU#WnO>d8B+d>D!h?Cw%()!{$6wq+3kaelv52418 zSaDXb_CPk=7i(`{vt9Er@32~7f5&BOzV3-s#mBf>((xzx!In1%(rJxrIJ1~=T}RT$ z!LrbYXD2&v>|HQk<@Gx*VeMg8&irm?x8zp33V#As%c{G&N6l#$`t{!=e9B3uEDS~< zesR|c&z)cdN>r7XmqX%E*1!`!(*>ITT5>|gzZmp4Q{(<`+((&>t9UyzQxpa0K9l^8dg5{69((5jn|qB4EOsaCP#A z{!J?=^u*fVv!~aD)r3`l56-_+^NBluyX~hNwUCa4g!W%t?0~c(#E;;slU;JBxmi$5 z1OgNkMnHfNVPO#<1Og@GXpkx);utGgCmRP8K`oFh&gvxd!{LPMlfzC1f>0P?B#(4a zL1Aoc36l^w5Tu7kIT`}R5I~U8PZ%Hqh5dw8fCv$SBzK$B$PkVcCftUaNY9@`!s7=) z+KF0($0`&Q2>JPx@%t`21%BOTzf$?1PZ~dm5Fi4K5GDxuAJ1j)jJ-Vd70YDY66(sU zW3@bTBx-Z^v&)zHDKK;~SMec%Ji&oH4-{f<0A-nAOqs5MVU(E)inJ7=ey|LxC7@>L zV8enuTX@|NQ!5Ie@{X3(MtU<5PHm$TxBM!P zQNh%x^TZ8u(=j_~aX^_xZA*9Lk1soEB!w~|Kk{aMYYRxaZO#_i8}7Wkl|1oInMnVk zf&Cuun6}h|16I@26@EHwMW~nYB}Q&fC@D^R;LF%BU5y{fW|Ih9Fg+?~RdKAU?655C zXi{p%bllE-I4L*Xk9;mIE!k-`b>e>FA3HCXQgXF#-K;l1)7Z=HN%Gi*>s*6BqddR= zx8e>W7PUN<^x;*Z-Nll}9on+I9S0>|)a^gC%glEhWh6j*{U*(Kk@q%qeV_1`w3HUP z25jhT$E$`Nuw7M!*nS9;m1?nzB(NYBorv^b4i14xheZcDVP&?gMgrF%kt5u1s4J~z zj*wdC*?12vcuMcveW+}(aD8H6H@^hQVER$F$+}6-8%k=k=4;8UMK*i>qF2np8AJcr z1Dcu{_uTs$C7UXrFWX!R-;t$mA&PvNLRaF9s2%*l*-i>l{>IAw95<7@sjcYsfjYwS zJhOY+Mk}JrOifuRaY}M=w{BC0B|imZDH%nx(%p1fIdz}KKE>pdmGyklCarShgoFv* zkRX-Mb}r8Ga}-W7S|DA5!HVj<1TmjEgT6#(2aiqObV_LhJ3YmQ*Tc#W%NglnQoEdn zIArG7(+?OA2RAKwohq*7^x$=pA#Ju0hmGALkFrff3}S?9+jv$UvXRQ=mZUhwR4HX- zm>~ApVE#M$z1y8qyFwAC|$_-1aCq zKQ)|w=8Qg<0QtjEK=h+%o8l-5S*ng8^7Kn$(X_I{%~N-8O!HWTMiX=0QsZ#d=gW)(lr|CaI=RpEK} z;NblLU!;dOANF}B+H~onuj7+td(-#zi}}H#We<1yIqE$O%wOq`$nM)^`M&9`6Axg; zn&ZDsuFgG4_U;ikom>?wutnou`yIu^=jKb%g-4GXF;$W7#y*`j_t)?{^YS(kf@o^Z z!Y5&FnsSH_1F6ZWOza|}h77Bwn}+#BqzMG983z-OJK z@ogbQx4%OYDP?%pcR@%V$@|;hlMD{`-!9Zbk%CM*RY{)H&(6N_KI?a;dV*&+a70mQ%5=SMcW8It_652~9u~Z99az)NHdS}&F6U9g zE`l?fVrbC*ibGIN@u57ER)+nB*ej}_A+<7y?3@rfaJVhGrIjW`ATj6}l~cez%{?oZ z%SU9#ob$}Ee3bW4!gSX9F{wybqfT!$UXDH38{*IcNd*FQRnRd?C25jsWzpu4p)R@X zpvt()2R}j`EfPu&mkTSBE0IS;ej##FN0oICeq!=qQUr7 zPgT-94!8vq@DZ%FO$Zx_;pS~OM$7z=`r)6#n+}tVZDvYh$xayRV6o7G$kz#Rz;D?J&bZiYnUKbp9cwWzf*dBV`+_&6i6yW9P zC6Y&6saBCVpW*fj_?^Q{DvH?|1urU1lzSa~X;Q-Wl>}1C!7`z#V7GAS@SEtZj>`_% zGdnIK6BFgr%Oj*gwu%E; zuU3{WpDz#Ez_u3yo1S%aWys-4`q*wgYu za573WhSMlmQB!bG+04mD9}(M}sHi^g4?~Y~xa$87`Xy3(7`x&CIgFoO7dGwpusPGw+{z1zC?p zwH@OBQ_|C50>4SZ}CFC)sgzCif8dKfx?4;%O;Uozeda&0O z3AKg2gm-u^4QCR+Pv~0!@I1CRVZCg4>*dB(efq{d$NOI}2t6b7CHD({;ni-*-dnN! z>R))(7|d33ZUo^ocpZ7vzJPjs@ugc+xz$~(LgMwl8!4P?q9Gvu8)N5Vq=RziYe^u4TL_}dq=2IM4*Z-VoIZ&(UjQ4pZhgfSHZs>*KYfo1u_>FwQ z0fH~nY>9A{xfbX7FV0P&JN7P{%#|yU%az%J85)gmY)TCc(n*EIUkS5#m7%JsGrc`J zN0Az#GoRt`IDz|CqjLh011A)rGVI{ws@@ZO|9+Wl)fk;mWJ5n`V^{#w$LkCmOw6{; z9uSiG5pu zKz^NhK=`*UXR65;{4@nec7gX}?_|cnZVo?n+a9D%+vX3xEvwe*w2S4gI51CEn$tIJ7LZN`+so}%scIJ9sGxPLd&ntyoGf3l z2nbBBzdT8LBAQZXm=xL+QFk@)k~w9Q;6R{@weBZ=8nXa#Wxl2XZBCZgij!CwNtd|c zpu$lIsP;VeKzC~U=3A0870J&isQWJ_CRn|E3zZNfPL9e(`SN9MYwUJpRi|-F-uqa6 z^-`MPw}r#w1Mr|sM2-yGo4qMoYu)DB6zS`d7kJXHc?K}G;yQS{bmEax%k(M1`_6TM zpz~=ke~E~BJ;t@l7xW(5mPCF)3x+ok|5B!FkA(Nl&LblIUw>m`X@#F*oMmY>^z5RH zP{YT3cvKub%u>Cpd=%7;ZmIA}pOzvk8)LYuEv@r>!nl~u?}|XT5cl<0U*fI5C-h;e zMiM7VpB<06mVgOgmTBIjcU`(wLU3T@GBd< z=%wsr5ulyw{+FwR4p(29Ya7XAmWu29#lCwu;L=2k8Qr76(++odW!^i(DSl6g?u!m{ z$2AMuT2-YuYT>+F>^)1muLDTM*o?wxg01>7?b0eeRvE(lK7m|-u$z*!h5!f7_tC{c z!m$$UjdDix=i+ZJB}KLiA;ME-~a@0CH87m(WO>DrlM5KSx`SZYAe{Tb|3c}mxd1BMjTG) z^~+Q(w(Z)?-&KzHJ=n3A`O#f^{7xe^MmogR$MB7#7iIhYGRPWIP+qlLopMq8n|sa5 zddzc+WA}B)5*u%)eImn{Mc;y6WUz(qM2R=e$>tl-2&5-G>{3^M#}A$Nh8((aR&if+1ma$0F^;fwPi>!@ z6bt&4^XcoGyj%C<^|-xY4s7gqEJ7YVmw0@&k=!n`ML9;qDL+tA@@fY2%5~!i3e)&7 zw`+y(ms6G{+RIHQhhM$2m1Bz36)$McL+SPSyO>_5ab&$ryZ*!V*>#ep$QlE)>;W^$ zZkd|HM$(|*w@q$}nuo9_W~|4QkmG1Xe^M#+SaAZclhfx1l=Fp2hkc(wD*ahX1Ov7U!S6bUsQ zBQ6P0!hgQb7RRB%Sj6~n9OT9wnX==Nx0cbQ|GGz$i>t4_^Qe~dh>G_aNVRESno^QQ zV2FbX!>+#1QU1rstI-5Tdh?6pbJBeWLs7z-`iL&M82Q4obBqzu>p4b_YV|#qZ{CH{ zNp%FiYa836bmmM}5=um*iyMTM=3DtFfH~gra%DlefB4%%Xg__tS9Dop=0^^N8w;E7 zB7>J@-oVFc4cI~}z1B4K70P+GPSm`kL#4Pv>bn|9R~N-LtJgV0{^ffJ_CXg5pUYR9GuDah zV_LbXBb;DtTTFO`KH4X}YARv09!W8lH47PKktX3E-q)10ns^^53`Q(^9z83W*d7-X zKk`}oaFpE?9(`n9D_ykpE$pJino&)%R86IHDfXZn(UKD!AD2Dw=+N*;G_?Lb+w9zg zCXT3`T3Wwgq>*}&9kvW!Z8NmKQ*#8(Gkh|eG;2O%yllQ`JiDa*ReN<{tI}JlcFb&i zb*r{APe4dV`cbDgX?_p7B>90?@Ie=YTlvCfyce^xQb~BHsz|-1$8$gm*Ik>3RX1+h zy^cIXs(Y^=qXpa7G)R(eOvecJA`ZV?|2A&|hzRCA>{tjYKC>-gMI@-(S4;v}xXO0! z=Iuo)?eE{6JKH|+ctv(EyxHS^pm&z%C`f+r_JPtwX*71BxC;$dZ;p%MxcB&(D&yhx zu-zK9i|y0g2jf(+_*7HQcwr6e@0`vWSWfLu9;>(7{`y0TPpD z3y;Mpw&loD1k%$}F4aD(<#*TkK7Q~qo4K}LY^VH+4Rqhjyx)qy`fbvM^0XI=e)Asl z-REY!dMlp0tWPbyW!q|F9@_R;-CdvKG5r)_BSKYRB+f#&*V zWQ=(Lf5~Fyj8DJng|O(aPZQKB6ODqDvP!3*t;5R#V7tg`FX`(qfU=DR47bGStM>-u zq;r6ic%?U1^VZ}yq%ORvRUe1c>3((JVUu!cun|zxf+KA9-~vM>LFUk-xzEKeUN4@2 zljEjR^Ai)wGrFIS7wtAntQQ=OOQ{;Q$(^&CH95XhLAAD>jHDHa=chm5biXgEeA zmH2w;jHC1hDzghq%e}oMKC7x=X0eeRk(A(q29@LeTJq^kru0UY`Xy~EAL8lC-RAqk z8oea9|5$$=(mC5%IE9f(=$wU^eIDz`pV>0o-`InjzWaXr-MG(1?N*XwaxP*p|9>fr z|7yRAfT92CPeOk6Ecpza@EE)U%KF-`f;ge_`?DC$N2nG9we^5TKnPd}Ap`~LxFNCl zJ3vBn@w89riT}OlSD~Fx*2x-+@&GDhF(`M`@A+Q^ZoZSIq@p$EFI#{07x@&#pLLFdY$?wrU6flCA2GL-SD=~ zSRkRudRi7I^h>SXtO)JrQ;P}n@`SE4p>7U@oOVI~9AliF6bQ{t0OV)i0Ho<`eKH3A z8mptcJe;wFPQa;pC#T5X)DZuTo~Wbym*#*c7KJ91z@bn8_&+b8sE7zm1c(OyiNO&k zrTdc?(CHTjhKdqm_IHfnsefT$g6sad4h$701oiKG!7x#RMSsU2q6mVI|A8Td3GVni z1{N0m7X}d(`L{ecp^W{Hy@bFLT=I8$1oi%1rCS*`wT+Y{r6rx7D=eDV^9B`K@W2WMTjL3r0eWVXbSxdHb~V8?M&zp zo!(`t3WOe!q%hbTEDQmo;YbM55^ODu5=NupXb}V$28W8mL@lKO|9i>LHU + + + + + + + + Cordova Mobile Spec + + + + +
+ + +
+ + + diff --git a/tests/tests.js b/tests/tests.js new file mode 100644 index 0000000..150a3c4 --- /dev/null +++ b/tests/tests.js @@ -0,0 +1,1018 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +/* global MSApp */ + +var cordova = require('cordova'); +var isWindows = cordova.platformId === 'windows'; +var isIos = cordova.platformId === 'ios'; +var isAndroid = cordova.platformId === 'android'; +var isBrowser = cordova.platformId === 'browser'; + +window.alert = window.alert || navigator.notification.alert; +if (isWindows && navigator && navigator.notification && navigator.notification.alert) { + // window.alert is defined but not functional on UWP + window.alert = navigator.notification.alert; +} + +exports.defineAutoTests = function () { + var createTests = function (platformOpts) { + platformOpts = platformOpts || ''; + + describe('cordova.InAppBrowser', function () { + it('inappbrowser.spec.1 should exist', function () { + expect(cordova.InAppBrowser).toBeDefined(); + }); + + it('inappbrowser.spec.2 should contain open function', function () { + expect(cordova.InAppBrowser.open).toBeDefined(); + expect(cordova.InAppBrowser.open).toEqual(jasmine.any(Function)); + }); + }); + + describe('open method', function () { + if (cordova.platformId === 'osx') { + pending('Open method not fully supported on OSX.'); + return; + } + + var iabInstance; + var originalTimeout; + var url = 'https://dist.apache.org/repos/dist/dev/cordova/'; + var badUrl = 'http://bad-uri/'; + + beforeEach(function () { + // increase timeout to ensure test url could be loaded within test time + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + + iabInstance = null; + }); + + afterEach(function (done) { + // restore original timeout + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + if (iabInstance !== null && iabInstance.close) { + iabInstance.close(); + } + iabInstance = null; + // add some extra time so that iab dialog is closed + setTimeout(done, 2000); + }); + + function verifyEvent (evt, type) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual(type); + // `exit` event does not have url field, browser returns null url for CORS requests + if (type !== 'exit' && !isBrowser) { + expect(evt.url).toEqual(url); + } + } + + function verifyLoadErrorEvent (evt) { + expect(evt).toBeDefined(); + expect(evt.type).toEqual('loaderror'); + expect(evt.url).toEqual(badUrl); + expect(evt.code).toEqual(jasmine.any(Number)); + expect(evt.message).toEqual(jasmine.any(String)); + } + + it('inappbrowser.spec.3 should return InAppBrowser instance with required methods', function () { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + + expect(iabInstance).toBeDefined(); + + expect(iabInstance.addEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.removeEventListener).toEqual(jasmine.any(Function)); + expect(iabInstance.close).toEqual(jasmine.any(Function)); + expect(iabInstance.show).toEqual(jasmine.any(Function)); + expect(iabInstance.hide).toEqual(jasmine.any(Function)); + expect(iabInstance.executeScript).toEqual(jasmine.any(Function)); + expect(iabInstance.insertCSS).toEqual(jasmine.any(Function)); + }); + + it('inappbrowser.spec.4 should support loadstart and loadstop events', function (done) { + var onLoadStart = jasmine.createSpy('loadstart event callback').and.callFake(function (evt) { + verifyEvent(evt, 'loadstart'); + }); + + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('loadstart', onLoadStart); + iabInstance.addEventListener('loadstop', function (evt) { + verifyEvent(evt, 'loadstop'); + if (!isBrowser) { + // according to documentation, "loadstart" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + expect(onLoadStart).toHaveBeenCalled(); + } + done(); + }); + }); + + it('inappbrowser.spec.5 should support exit event', function (done) { + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('exit', function (evt) { + verifyEvent(evt, 'exit'); + done(); + }); + iabInstance.addEventListener('loadstop', function (evt) { + iabInstance.close(); + iabInstance = null; + }); + }); + + it('inappbrowser.spec.6 should support loaderror event', function (done) { + if (isBrowser) { + // according to documentation, "loaderror" event is not supported on browser + // https://github.com/apache/cordova-plugin-inappbrowser#browser-quirks-1 + pending("Browser platform doesn't support loaderror event"); + } + iabInstance = cordova.InAppBrowser.open(badUrl, '_blank', platformOpts); + iabInstance.addEventListener('loaderror', function (evt) { + verifyLoadErrorEvent(evt); + done(); + }); + }); + + it('inappbrowser.spec.7 should support message event', function (done) { + if (!isAndroid && !isIos) { + return pending(cordova.platformId + " platform doesn't support message event"); + } + var messageKey = 'my_message'; + var messageValue = 'is_this'; + iabInstance = cordova.InAppBrowser.open(url, '_blank', platformOpts); + iabInstance.addEventListener('message', function (evt) { + // Verify message event + expect(evt).toBeDefined(); + expect(evt.type).toEqual('message'); + expect(evt.data).toBeDefined(); + expect(evt.data[messageKey]).toBeDefined(); + expect(evt.data[messageKey]).toEqual(messageValue); + done(); + }); + iabInstance.addEventListener('loadstop', function (evt) { + var code = + '(function(){\n' + + ' var message = {' + + messageKey + + ': "' + + messageValue + + '"};\n' + + ' webkit.messageHandlers.cordova_iab.postMessage(JSON.stringify(message));\n' + + '})()'; + iabInstance.executeScript({ code: code }); + }); + }); + }); + }; + createTests(); +}; + +exports.defineManualTests = function (contentEl, createActionButton) { + var platformOpts = ''; + var platform_info = ''; + + function doOpen (url, target, params, numExpectedRedirects, useWindowOpen) { + numExpectedRedirects = numExpectedRedirects || 0; + useWindowOpen = useWindowOpen || false; + console.log('Opening ' + url); + + var counts; + var lastLoadStartURL; + var wasReset = false; + function reset () { + counts = { + loaderror: 0, + loadstart: 0, + loadstop: 0, + exit: 0 + }; + lastLoadStartURL = ''; + } + reset(); + + var iab; + var callbacks = { + loaderror: logEvent, + loadstart: logEvent, + loadstop: logEvent, + exit: logEvent + }; + if (useWindowOpen) { + console.log('Use window.open() for url'); + iab = window.open(url, target, params, callbacks); + } else { + if (platformOpts) { + params += (params ? ',' : '') + platformOpts; + } + iab = cordova.InAppBrowser.open(url, target, params, callbacks); + } + if (!iab) { + alert('open returned ' + iab); // eslint-disable-line no-undef + return; + } + + function logEvent (e) { + console.log('IAB event=' + JSON.stringify(e)); + counts[e.type]++; + // Verify that event.url gets updated on redirects. + if (e.type === 'loadstart') { + if (e.url === lastLoadStartURL) { + alert('Unexpected: loadstart fired multiple times for the same URL.'); // eslint-disable-line no-undef + } + lastLoadStartURL = e.url; + } + // Verify the right number of loadstart events were fired. + if (e.type === 'loadstop' || e.type === 'loaderror') { + if (e.url !== lastLoadStartURL) { + alert('Unexpected: ' + e.type + " event.url != loadstart's event.url"); // eslint-disable-line no-undef + } + if (numExpectedRedirects === 0 && counts.loadstart !== 1) { + // Do allow a loaderror without a loadstart (e.g. in the case of an invalid URL). + if (!(e.type === 'loaderror' && counts.loadstart === 0)) { + alert('Unexpected: got multiple loadstart events. (' + counts.loadstart + ')'); // eslint-disable-line no-undef + } + } else if (numExpectedRedirects > 0 && counts.loadstart < numExpectedRedirects + 1) { + alert( + 'Unexpected: should have got at least ' + + (numExpectedRedirects + 1) + + ' loadstart events, but got ' + + counts.loadstart + ); // eslint-disable-line no-undef + } + wasReset = true; + numExpectedRedirects = 0; + reset(); + } + // Verify that loadend / loaderror was called. + if (e.type === 'exit') { + var numStopEvents = counts.loadstop + counts.loaderror; + if (numStopEvents === 0 && !wasReset) { + alert('Unexpected: browser closed without a loadstop or loaderror.'); // eslint-disable-line no-undef + } else if (numStopEvents > 1) { + alert('Unexpected: got multiple loadstop/loaderror events.'); // eslint-disable-line no-undef + } + } + } + + return iab; + } + + function doHookOpen (url, target, params, numExpectedRedirects) { + var originalFunc = window.open; + var wasClobbered = Object.prototype.hasOwnProperty.call(window, 'open'); + window.open = cordova.InAppBrowser.open; + + try { + doOpen(url, target, params, numExpectedRedirects, true); + } finally { + if (wasClobbered) { + window.open = originalFunc; + } else { + console.log('just delete, to restore open from prototype'); + delete window.open; + } + } + } + + function openWithStyle (url, cssUrl, useCallback) { + var iab = doOpen(url, '_blank', 'location=yes'); + var callback = function (results) { + if (results && results.length === 0) { + alert('Results verified'); // eslint-disable-line no-undef + } else { + console.log(results); + alert('Got: ' + typeof results + '\n' + JSON.stringify(results)); // eslint-disable-line no-undef + } + }; + if (cssUrl) { + iab.addEventListener('loadstop', function (event) { + iab.insertCSS({ file: cssUrl }, useCallback && callback); + }); + } else { + iab.addEventListener('loadstop', function (event) { + iab.insertCSS({ code: '#style-update-literal { \ndisplay: block !important; \n}' }, useCallback && callback); + }); + } + } + + function openWithScript (url, jsUrl, useCallback) { + var iab = doOpen(url, '_blank', 'location=yes'); + if (jsUrl) { + iab.addEventListener('loadstop', function (event) { + iab.executeScript( + { file: jsUrl }, + useCallback && + function (results) { + if (results && results.length === 0) { + alert('Results verified'); // eslint-disable-line no-undef + } else { + console.log(results); + alert('Got: ' + typeof results + '\n' + JSON.stringify(results)); // eslint-disable-line no-undef + } + } + ); + }); + } else { + iab.addEventListener('loadstop', function (event) { + var code = + '(function(){\n' + + ' var header = document.getElementById("header");\n' + + ' header.innerHTML = "Script literal successfully injected";\n' + + ' return "abc";\n' + + '})()'; + iab.executeScript( + { code: code }, + useCallback && + function (results) { + if (results && results.length === 1 && results[0] === 'abc') { + alert('Results verified'); // eslint-disable-line no-undef + } else { + console.log(results); + alert('Got: ' + typeof results + '\n' + JSON.stringify(results)); // eslint-disable-line no-undef + } + } + ); + }); + } + } + var hiddenwnd = null; + var loadlistener = function (event) { + alert('background window loaded '); + }; // eslint-disable-line no-undef + function openHidden (url, startHidden) { + var shopt = startHidden ? 'hidden=yes' : ''; + if (platformOpts) { + shopt += (shopt ? ',' : '') + platformOpts; + } + hiddenwnd = cordova.InAppBrowser.open(url, 'random_string', shopt); + if (!hiddenwnd) { + alert('cordova.InAppBrowser.open returned ' + hiddenwnd); // eslint-disable-line no-undef + return; + } + if (startHidden) hiddenwnd.addEventListener('loadstop', loadlistener); + } + function showHidden () { + if (hiddenwnd) { + hiddenwnd.show(); + } + } + function closeHidden () { + if (hiddenwnd) { + hiddenwnd.removeEventListener('loadstop', loadlistener); + hiddenwnd.close(); + hiddenwnd = null; + } + } + + var info_div = + '

InAppBrowser

' + + '
' + + 'Make sure http://cordova.apache.org and http://google.co.uk and https://www.google.co.uk are white listed.
' + + 'Make sure http://www.apple.com is not in the white list.
' + + 'In iOS, starred * tests will put the app in a state with no way to return.
' + + '

User-Agent: ' + + '

'; + + var local_tests = + '

Local URL

' + + '
' + + 'Expected result: opens successfully in CordovaWebView.' + + '

' + + 'Expected result: opens successfully in CordovaWebView (using hook of window.open()).' + + '

' + + 'Expected result: opens successfully in CordovaWebView.' + + '

' + + 'Expected result: fails to open' + + '

' + + 'Expected result: opens successfully in InAppBrowser with locationBar at top.' + + '

' + + 'Expected result: opens successfully in InAppBrowser without locationBar.' + + '

' + + 'Expected result: opens successfully in InAppBrowser with locationBar. On iOS the toolbar is at the bottom.' + + '

' + + 'Expected result: opens successfully in InAppBrowser with locationBar. On iOS the toolbar is at the top.' + + '

' + + 'Expected result: open successfully in InAppBrowser with no locationBar. On iOS the toolbar is at the top.'; + + var white_listed_tests = + '

White Listed URL

' + + '
' + + 'Expected result: open successfully in CordovaWebView to cordova.apache.org' + + '

' + + 'Expected result: open successfully in CordovaWebView to cordova.apache.org (using hook of window.open())' + + '

' + + 'Expected result: open successfully in CordovaWebView to cordova.apache.org' + + '

' + + 'Expected result: open successfully in system browser to cordova.apache.org' + + '

' + + 'Expected result: open successfully in InAppBrowser to cordova.apache.org' + + '

' + + 'Expected result: open successfully in InAppBrowser to cordova.apache.org' + + '

' + + 'Expected result: open successfully in InAppBrowser to cordova.apache.org with no location bar.'; + + var non_white_listed_tests = + '

Non White Listed URL

' + + '
' + + 'Expected result: open successfully in InAppBrowser to apple.com.' + + '

' + + 'Expected result: open successfully in InAppBrowser to apple.com (using hook of window.open()).' + + '

' + + 'Expected result: open successfully in InAppBrowser to apple.com (_self enforces whitelist).' + + '

' + + 'Expected result: open successfully in system browser to apple.com.' + + '

' + + 'Expected result: open successfully in InAppBrowser to apple.com.' + + '

' + + 'Expected result: open successfully in InAppBrowser to apple.com.' + + '

' + + 'Expected result: open successfully in InAppBrowser to apple.com without locationBar.'; + + var page_with_redirects_tests = + '

Page with redirect

' + + '
' + + 'Expected result: should 301 and open successfully in InAppBrowser to https://www.google.co.uk.' + + '

' + + 'Expected result: should 302 and open successfully in InAppBrowser to www.zhihu.com/answer/16714076.'; + + var pdf_url_tests = + '

PDF URL

' + + '
' + + 'Expected result: InAppBrowser opens. PDF should render on iOS.' + + '

' + + 'Expected result: InAppBrowser opens. PDF should render on iOS.'; + + var invalid_url_tests = + '

Invalid URL

' + + '
' + + 'Expected result: fail to load in InAppBrowser.' + + '

' + + 'Expected result: fail to load in InAppBrowser.' + + '

' + + 'Expected result: fail to load in InAppBrowser (404).'; + + var css_js_injection_tests = + '

CSS / JS Injection

' + + '
' + + 'Expected result: open successfully in InAppBrowser without text "Style updated from..."' + + '

' + + 'Expected result: open successfully in InAppBrowser with "Style updated from file".' + + '

' + + 'Expected result: open successfully in InAppBrowser with "Style updated from file", and alert dialog with text "Results verified".' + + '

' + + 'Expected result: open successfully in InAppBrowser with "Style updated from literal".' + + '

' + + 'Expected result: open successfully in InAppBrowser with "Style updated from literal", and alert dialog with text "Results verified".' + + '

' + + 'Expected result: open successfully in InAppBrowser with text "Script file successfully injected".' + + '

' + + 'Expected result: open successfully in InAppBrowser with text "Script file successfully injected" and alert dialog with the text "Results verified".' + + '

' + + 'Expected result: open successfully in InAppBrowser with the text "Script literal successfully injected" .' + + '

' + + 'Expected result: open successfully in InAppBrowser with the text "Script literal successfully injected" and alert dialog with the text "Results verified".'; + + var open_hidden_tests = + '

Open Hidden

' + + '
' + + 'Expected result: no additional browser window. Alert appears with the text "background window loaded".' + + '

' + + 'Expected result: after first clicking on previous test "create hidden", open successfully in InAppBrowser to https://www.google.co.uk.' + + '

' + + 'Expected result: no output. But click on "show hidden" again and nothing should be shown.' + + '

' + + 'Expected result: open successfully in InAppBrowser to https://www.google.co.uk' + + '

' + + 'Expected result: open successfully in InAppBrowser to https://www.google.co.uk. Hide after 2 seconds'; + + var clearing_cache_tests = + '

Clearing Cache

' + + '
' + + 'Expected result: ?' + + '

' + + 'Expected result: ?'; + + var video_tag_tests = + '

Video tag

' + + '
' + + 'Expected result: open successfully in InAppBrowser with an embedded video plays automatically on iOS and Android.' + + '
' + + 'Expected result: open successfully in InAppBrowser with an embedded video plays automatically on iOS and Android.' + + '
' + + 'Expected result: open successfully in InAppBrowser with an embedded video does not play automatically on iOS and Android but rather works after clicking the "play" button.'; + + var local_with_anchor_tag_tests = + '

Local with anchor tag

' + + '
' + + 'Expected result: open successfully in InAppBrowser to the local page, scrolled to the top as normal.' + + '

' + + 'Expected result: open successfully in InAppBrowser to the local page, scrolled to the beginning of the tall div with border.'; + + var hardwareback_tests = + '

HardwareBack

' + + '

' + + 'Expected result: By default hardwareback is yes so pressing back button should navigate backwards in history then close InAppBrowser' + + '

' + + 'Expected result: hardwareback=yes pressing back button should navigate backwards in history then close InAppBrowser' + + '

' + + 'Expected result: hardwareback=no pressing back button should close InAppBrowser regardless history' + + '

' + + 'Expected result: consistently open browsers with with the appropriate option: hardwareback=defaults to yes then hardwareback=no then hardwareback=defaults to yes. By default hardwareback is yes so pressing back button should navigate backwards in history then close InAppBrowser'; + + // CB-7490 We need to wrap this code due to Windows security restrictions + // see http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx#differences for details + if (window.MSApp && window.MSApp.execUnsafeLocalFunction) { + MSApp.execUnsafeLocalFunction(function () { + contentEl.innerHTML = + info_div + + platform_info + + local_tests + + white_listed_tests + + non_white_listed_tests + + page_with_redirects_tests + + pdf_url_tests + + invalid_url_tests + + css_js_injection_tests + + open_hidden_tests + + clearing_cache_tests + + video_tag_tests + + local_with_anchor_tag_tests + + hardwareback_tests; + }); + } else { + contentEl.innerHTML = + info_div + + platform_info + + local_tests + + white_listed_tests + + non_white_listed_tests + + page_with_redirects_tests + + pdf_url_tests + + invalid_url_tests + + css_js_injection_tests + + open_hidden_tests + + clearing_cache_tests + + video_tag_tests + + local_with_anchor_tag_tests + + hardwareback_tests; + } + + document.getElementById('user-agent').textContent = navigator.userAgent; + + // we are already in cdvtests directory + var basePath = 'iab-resources/'; + var localhtml = basePath + 'local.html'; + var localpdf = basePath + 'local.pdf'; + var injecthtml = basePath + 'inject.html'; + var injectjs = isWindows ? basePath + 'inject.js' : 'inject.js'; + var injectcss = isWindows ? basePath + 'inject.css' : 'inject.css'; + var videohtml = basePath + 'video.html'; + + // Local + createActionButton( + 'target=Default', + function () { + doOpen(localhtml); + }, + 'openLocal' + ); + createActionButton( + 'target=Default (window.open)', + function () { + doHookOpen(localhtml); + }, + 'openLocalHook' + ); + createActionButton( + 'target=_self', + function () { + doOpen(localhtml, '_self'); + }, + 'openLocalSelf' + ); + createActionButton( + 'target=_system', + function () { + doOpen(localhtml, '_system'); + }, + 'openLocalSystem' + ); + createActionButton( + 'target=_blank', + function () { + doOpen(localhtml, '_blank'); + }, + 'openLocalBlank' + ); + createActionButton( + 'target=Random, location=no, disallowoverscroll=yes', + function () { + doOpen(localhtml, 'random_string', 'location=no, disallowoverscroll=yes'); + }, + 'openLocalRandomNoLocation' + ); + createActionButton( + 'target=Random, toolbarposition=bottom', + function () { + doOpen(localhtml, 'random_string', 'toolbarposition=bottom'); + }, + 'openLocalRandomToolBarBottom' + ); + createActionButton( + 'target=Random, toolbarposition=top', + function () { + doOpen(localhtml, 'random_string', 'toolbarposition=top'); + }, + 'openLocalRandomToolBarTop' + ); + createActionButton( + 'target=Random, toolbarposition=top, location=no', + function () { + doOpen(localhtml, 'random_string', 'toolbarposition=top,location=no'); + }, + 'openLocalRandomToolBarTopNoLocation' + ); + + // White Listed + createActionButton( + '* target=Default', + function () { + doOpen('http://cordova.apache.org'); + }, + 'openWhiteListed' + ); + createActionButton( + '* target=Default (window.open)', + function () { + doHookOpen('http://cordova.apache.org'); + }, + 'openWhiteListedHook' + ); + createActionButton( + '* target=_self', + function () { + doOpen('http://cordova.apache.org', '_self'); + }, + 'openWhiteListedSelf' + ); + createActionButton( + 'target=_system', + function () { + doOpen('http://cordova.apache.org', '_system'); + }, + 'openWhiteListedSystem' + ); + createActionButton( + 'target=_blank', + function () { + doOpen('http://cordova.apache.org', '_blank'); + }, + 'openWhiteListedBlank' + ); + createActionButton( + 'target=Random', + function () { + doOpen('http://cordova.apache.org', 'random_string'); + }, + 'openWhiteListedRandom' + ); + createActionButton( + '* target=Random, no location bar', + function () { + doOpen('http://cordova.apache.org', 'random_string', 'location=no'); + }, + 'openWhiteListedRandomNoLocation' + ); + + // Non White Listed + createActionButton( + 'target=Default', + function () { + doOpen('http://www.apple.com'); + }, + 'openNonWhiteListed' + ); + createActionButton( + 'target=Default (window.open)', + function () { + doHookOpen('http://www.apple.com'); + }, + 'openNonWhiteListedHook' + ); + createActionButton( + 'target=_self', + function () { + doOpen('http://www.apple.com', '_self'); + }, + 'openNonWhiteListedSelf' + ); + createActionButton( + 'target=_system', + function () { + doOpen('http://www.apple.com', '_system'); + }, + 'openNonWhiteListedSystem' + ); + createActionButton( + 'target=_blank', + function () { + doOpen('http://www.apple.com', '_blank'); + }, + 'openNonWhiteListedBlank' + ); + createActionButton( + 'target=Random', + function () { + doOpen('http://www.apple.com', 'random_string'); + }, + 'openNonWhiteListedRandom' + ); + createActionButton( + '* target=Random, no location bar', + function () { + doOpen('http://www.apple.com', 'random_string', 'location=no'); + }, + 'openNonWhiteListedRandomNoLocation' + ); + + // Page with redirect + createActionButton( + 'http://google.co.uk', + function () { + doOpen('http://google.co.uk', 'random_string', '', 1); + }, + 'openRedirect301' + ); + createActionButton( + 'http://goo.gl/pUFqg', + function () { + doOpen('http://goo.gl/pUFqg', 'random_string', '', 2); + }, + 'openRedirect302' + ); + + // PDF URL + createActionButton( + 'Remote URL', + function () { + doOpen('http://www.stluciadance.com/prospectus_file/sample.pdf'); + }, + 'openPDF' + ); + createActionButton( + 'Local URL', + function () { + doOpen(localpdf, '_blank'); + }, + 'openPDFBlank' + ); + + // Invalid URL + createActionButton( + 'Invalid Scheme', + function () { + doOpen('x-ttp://www.invalid.com/', '_blank'); + }, + 'openInvalidScheme' + ); + createActionButton( + 'Invalid Host', + function () { + doOpen('http://www.inv;alid.com/', '_blank'); + }, + 'openInvalidHost' + ); + createActionButton( + 'Missing Local File', + function () { + doOpen('nonexistent.html', '_blank'); + }, + 'openInvalidMissing' + ); + + // CSS / JS injection + createActionButton( + 'Original Document', + function () { + doOpen(injecthtml, '_blank'); + }, + 'openOriginalDocument' + ); + createActionButton( + 'CSS File Injection', + function () { + openWithStyle(injecthtml, injectcss); + }, + 'openCSSInjection' + ); + createActionButton( + 'CSS File Injection (callback)', + function () { + openWithStyle(injecthtml, injectcss, true); + }, + 'openCSSInjectionCallback' + ); + createActionButton( + 'CSS Literal Injection', + function () { + openWithStyle(injecthtml); + }, + 'openCSSLiteralInjection' + ); + createActionButton( + 'CSS Literal Injection (callback)', + function () { + openWithStyle(injecthtml, null, true); + }, + 'openCSSLiteralInjectionCallback' + ); + createActionButton( + 'Script File Injection', + function () { + openWithScript(injecthtml, injectjs); + }, + 'openScriptInjection' + ); + createActionButton( + 'Script File Injection (callback)', + function () { + openWithScript(injecthtml, injectjs, true); + }, + 'openScriptInjectionCallback' + ); + createActionButton( + 'Script Literal Injection', + function () { + openWithScript(injecthtml); + }, + 'openScriptLiteralInjection' + ); + createActionButton( + 'Script Literal Injection (callback)', + function () { + openWithScript(injecthtml, null, true); + }, + 'openScriptLiteralInjectionCallback' + ); + + // Open hidden + createActionButton( + 'Create Hidden', + function () { + openHidden('https://www.google.co.uk', true); + }, + 'openHidden' + ); + createActionButton( + 'Show Hidden', + function () { + showHidden(); + }, + 'showHidden' + ); + createActionButton( + 'Close Hidden', + function () { + closeHidden(); + }, + 'closeHidden' + ); + createActionButton( + 'google.co.uk Not Hidden', + function () { + openHidden('https://www.google.co.uk', false); + }, + 'openHiddenShow' + ); + createActionButton( + 'google.co.uk shown for 2 seconds than hidden', + function () { + var iab = doOpen('https://www.google.co.uk/', 'random_sting'); + setTimeout(function () { + iab.hide(); + }, 2000); + }, + 'openVisibleAndHide' + ); + + // Clearing cache + createActionButton( + 'Clear Browser Cache', + function () { + doOpen('https://www.google.co.uk', '_blank', 'clearcache=yes'); + }, + 'openClearCache' + ); + createActionButton( + 'Clear Session Cache', + function () { + doOpen('https://www.google.co.uk', '_blank', 'clearsessioncache=yes'); + }, + 'openClearSessionCache' + ); + + // Video tag + createActionButton( + 'Remote Video', + function () { + doOpen(videohtml, '_blank'); + }, + 'openRemoteVideo' + ); + createActionButton( + 'Remote Need User No Video', + function () { + doOpen(videohtml, '_blank', 'mediaPlaybackRequiresUserAction=no'); + }, + 'openRemoteNeedUserNoVideo' + ); + createActionButton( + 'Remote Need User Yes Video', + function () { + doOpen(videohtml, '_blank', 'mediaPlaybackRequiresUserAction=yes'); + }, + 'openRemoteNeedUserYesVideo' + ); + + // Local With Anchor Tag + createActionButton( + 'Anchor1', + function () { + doOpen(localhtml + '#bogusanchor', '_blank'); + }, + 'openAnchor1' + ); + createActionButton( + 'Anchor2', + function () { + doOpen(localhtml + '#anchor2', '_blank'); + }, + 'openAnchor2' + ); + + // Hardwareback + createActionButton( + 'no hardwareback (defaults to yes)', + function () { + doOpen('http://cordova.apache.org', '_blank'); + }, + 'openHardwareBackDefault' + ); + createActionButton( + 'hardwareback=yes', + function () { + doOpen('http://cordova.apache.org', '_blank', 'hardwareback=yes'); + }, + 'openHardwareBackYes' + ); + createActionButton( + 'hardwareback=no', + function () { + doOpen('http://cordova.apache.org', '_blank', 'hardwareback=no'); + }, + 'openHardwareBackNo' + ); + createActionButton( + 'no hardwareback -> hardwareback=no -> no hardwareback', + function () { + var ref = cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); + ref.addEventListener('loadstop', function () { + ref.close(); + }); + ref.addEventListener('exit', function () { + var ref2 = cordova.InAppBrowser.open( + 'https://google.com', + '_blank', + 'location=yes,hardwareback=no' + (platformOpts ? ',' + platformOpts : '') + ); + ref2.addEventListener('loadstop', function () { + ref2.close(); + }); + ref2.addEventListener('exit', function () { + cordova.InAppBrowser.open('https://google.com', '_blank', 'location=yes' + (platformOpts ? ',' + platformOpts : '')); + }); + }); + }, + 'openHardwareBackDefaultAfterNo' + ); +}; diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..dacda6d --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,109 @@ +// Type definitions for Apache Cordova InAppBrowser plugin +// Project: https://github.com/apache/cordova-plugin-inappbrowser +// Definitions by: Microsoft Open Technologies Inc +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// +// Copyright (c) Microsoft Open Technologies Inc +// Licensed under the MIT license. +// TypeScript Version: 2.3 +type channel = "loadstart" | "loadstop" | "loaderror" | "exit" | "message" | "customscheme"; + +/** + * The object returned from a call to cordova.InAppBrowser.open. + * NOTE: The InAppBrowser window behaves like a standard web browser, and can't access Cordova APIs. + */ +interface InAppBrowser { + + /** + * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. + * @param url The URL to load. + * @param target The target in which to load the URL, an optional parameter that defaults to _self. + * @param options Options for the InAppBrowser. Optional, defaulting to: location=yes. + * The options string must not contain any blank space, and each feature's + * name/value pairs must be separated by a comma. Feature names are case insensitive. + */ + open(url: string, target?: string, options?: string): InAppBrowser; + + onloadstart(type: Event): void; + onloadstop(type: InAppBrowserEvent): void; + onloaderror(type: InAppBrowserEvent): void; + onexit(type: InAppBrowserEvent): void; + // addEventListener overloads + /** + * Adds a listener for an event from the InAppBrowser. + * @param type loadstart: event fires when the InAppBrowser starts to load a URL. + * loadstop: event fires when the InAppBrowser finishes loading a URL. + * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. + * exit: event fires when the InAppBrowser window is closed. + * @param callback the function that executes when the event fires. The function is + * passed an InAppBrowserEvent object as a parameter. + */ + addEventListener(type: channel, callback: InAppBrowserEventListenerOrEventListenerObject): void; + /** + * Adds a listener for an event from the InAppBrowser. + * @param type any custom event that might occur. + * @param callback the function that executes when the event fires. The function is + * passed an InAppBrowserEvent object as a parameter. + */ + addEventListener(type: string, callback: InAppBrowserEventListenerOrEventListenerObject): void; + // removeEventListener overloads + /** + * Removes a listener for an event from the InAppBrowser. + * @param type The event to stop listening for. + * loadstart: event fires when the InAppBrowser starts to load a URL. + * loadstop: event fires when the InAppBrowser finishes loading a URL. + * loaderror: event fires when the InAppBrowser encounters an error when loading a URL. + * exit: event fires when the InAppBrowser window is closed. + * @param callback the function that executes when the event fires. The function is + * passed an InAppBrowserEvent object as a parameter. + */ + removeEventListener(type: channel, callback: InAppBrowserEventListenerOrEventListenerObject): void; + /** Closes the InAppBrowser window. */ + close(): void; + /** Hides the InAppBrowser window. Calling this has no effect if the InAppBrowser was already hidden. */ + hide(): void; + /** + * Displays an InAppBrowser window that was opened hidden. Calling this has no effect + * if the InAppBrowser was already visible. + */ + show(): void; + /** + * Injects JavaScript code into the InAppBrowser window. + * @param script Details of the script to run, specifying either a file or code key. + * @param callback The function that executes after the JavaScript code is injected. + * If the injected script is of type code, the callback executes with + * a single parameter, which is the return value of the script, wrapped in an Array. + * For multi-line scripts, this is the return value of the last statement, + * or the last expression evaluated. + */ + executeScript(script: { code: string } | { file: string }, callback: (result: any) => void): void; + /** + * Injects CSS into the InAppBrowser window. + * @param css Details of the script to run, specifying either a file or code key. + * @param callback The function that executes after the CSS is injected. + */ + insertCSS(css: { code: string } | { file: string }, callback: () => void): void; +} + +type InAppBrowserEventListenerOrEventListenerObject = InAppBrowserEventListener | InAppBrowserEventListenerObject; + +type InAppBrowserEventListener = (evt: InAppBrowserEvent) => void; + +interface InAppBrowserEventListenerObject { + handleEvent(evt: InAppBrowserEvent): void; +} + +interface InAppBrowserEvent extends Event { + /** the eventname, either loadstart, loadstop, loaderror, or exit. */ + type: string; + /** the URL that was loaded. */ + url: string; + /** the error code, only in the case of loaderror. */ + code: number; + /** the error message, only in the case of loaderror. */ + message: string; +} + +interface Cordova { + InAppBrowser: InAppBrowser; +} diff --git a/www/inappbrowser.css b/www/inappbrowser.css new file mode 100644 index 0000000..5762c74 --- /dev/null +++ b/www/inappbrowser.css @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.inAppBrowserWrap { + margin: 0; + padding: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: 0 0; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999999; + box-sizing: border-box; + border: 40px solid #bfbfbf; + border: 40px solid rgba(0, 0, 0, 0.25); +} + +.inAppBrowserWrapFullscreen { + border: 0; +} + +.inappbrowser-app-bar { + height: 70px; + background-color: #404040; + z-index: 9999999; +} + +.inappbrowser-app-bar-inner { + padding-top: 10px; + height: 60px; + width: 155px; + margin: 0 auto; + background-color: #404040; + z-index: 9999999; +} + +.app-bar-action { + width: auto; + height: 40px; + margin-left: 20px; + font-family: "Segoe UI Symbol"; + float: left; + color: white; + font-size: 12px; + text-transform: lowercase; + text-align: center; + cursor: default; +} + +.app-bar-action[disabled] { + color: gray; + /*disable click*/ + pointer-events: none; +} + +.app-bar-action::before { + font-size: 28px; + display: block; + height: 36px; +} + +/* Back */ +.action-back { + margin-left: 0px; +} + +.action-back::before { + content: "\E0BA"; +} + +.action-back:not([disabled]):hover::before { + content: "\E0B3"; +} + +/* Forward */ +.action-forward::before { + content: "\E0AC"; +} + +.action-forward:not([disabled]):hover::before { + content: "\E0AF"; +} + +/* Close */ +.action-close::before { + content: "\E0C7"; + /* close icon is larger so we re-size it to fit other icons */ + font-size: 20px; + line-height: 40px; +} + +.action-close:not([disabled]):hover::before { + content: "\E0CA"; +} diff --git a/www/inappbrowser.js b/www/inappbrowser.js new file mode 100644 index 0000000..3dcab21 --- /dev/null +++ b/www/inappbrowser.js @@ -0,0 +1,119 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +(function () { + var exec = require('cordova/exec'); + var channel = require('cordova/channel'); + var modulemapper = require('cordova/modulemapper'); + var urlutil = require('cordova/urlutil'); + + function InAppBrowser () { + this.channels = { + beforeload: channel.create('beforeload'), + loadstart: channel.create('loadstart'), + loadstop: channel.create('loadstop'), + loaderror: channel.create('loaderror'), + exit: channel.create('exit'), + customscheme: channel.create('customscheme'), + message: channel.create('message') + }; + } + + InAppBrowser.prototype = { + _eventHandler: function (event) { + if (event && event.type in this.channels) { + if (event.type === 'beforeload') { + this.channels[event.type].fire(event, this._loadAfterBeforeload); + } else { + this.channels[event.type].fire(event); + } + } + }, + _loadAfterBeforeload: function (strUrl) { + strUrl = urlutil.makeAbsolute(strUrl); + exec(null, null, 'InAppBrowser', 'loadAfterBeforeload', [strUrl]); + }, + close: function (eventname) { + exec(null, null, 'InAppBrowser', 'close', []); + }, + show: function (eventname) { + exec(null, null, 'InAppBrowser', 'show', []); + }, + hide: function (eventname) { + exec(null, null, 'InAppBrowser', 'hide', []); + }, + addEventListener: function (eventname, f) { + if (eventname in this.channels) { + this.channels[eventname].subscribe(f); + } + }, + removeEventListener: function (eventname, f) { + if (eventname in this.channels) { + this.channels[eventname].unsubscribe(f); + } + }, + + executeScript: function (injectDetails, cb) { + if (injectDetails.code) { + exec(cb, null, 'InAppBrowser', 'injectScriptCode', [injectDetails.code, !!cb]); + } else if (injectDetails.file) { + exec(cb, null, 'InAppBrowser', 'injectScriptFile', [injectDetails.file, !!cb]); + } else { + throw new Error('executeScript requires exactly one of code or file to be specified'); + } + }, + + insertCSS: function (injectDetails, cb) { + if (injectDetails.code) { + exec(cb, null, 'InAppBrowser', 'injectStyleCode', [injectDetails.code, !!cb]); + } else if (injectDetails.file) { + exec(cb, null, 'InAppBrowser', 'injectStyleFile', [injectDetails.file, !!cb]); + } else { + throw new Error('insertCSS requires exactly one of code or file to be specified'); + } + } + }; + + module.exports = function (strUrl, strWindowName, strWindowFeatures, callbacks) { + // Don't catch calls that write to existing frames (e.g. named iframes). + if (window.frames && window.frames[strWindowName]) { + var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open'); + return origOpenFunc.apply(window, arguments); + } + + strUrl = urlutil.makeAbsolute(strUrl); + var iab = new InAppBrowser(); + + callbacks = callbacks || {}; + for (var callbackName in callbacks) { + iab.addEventListener(callbackName, callbacks[callbackName]); + } + + var cb = function (eventname) { + iab._eventHandler(eventname); + }; + + strWindowFeatures = strWindowFeatures || ''; + + exec(cb, cb, 'InAppBrowser', 'open', [strUrl, strWindowName, strWindowFeatures]); + return iab; + }; +})();