diff --git a/README.md b/README.md index 684951b70..66e99d373 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ cordova-plugin-themeablebrowser =============================== -**This repo is out of maintenance** due to its original mainteners are no longer able to maintain it in an acceptable fashion. Please consider forking this repo if it interests you. Apologies to everyone who still depends on this repo and thanks to everyone who has contributed. - ---- - This plugin is a fork of [org.apache.cordova.inappbrowser](https://github.com/apache/cordova-plugin-inappbrowser). It attempts to retain most of the features of the InAppBrowser. In fact, for the full list of features inherited from InAppBrowser, please refer to [InAppBrowser's documentation](https://github.com/apache/cordova-plugin-inappbrowser/blob/master/README.md). The purpose of this plugin is to provide an in-app-browser that can also be configured to match the theme of your app, in order to give it a more immersive look and feel for your app, as well as provide a more consistent look and feel across platforms. @@ -378,4 +374,4 @@ One is redefined. License ------- -This project is licensed under Aapache License 2.0. See [LICENSE](LICENSE) file. +This project is licensed under Aapache License 2.0. See [LICENSE](LICENSE) file. \ No newline at end of file diff --git a/package.json b/package.json index 8fd3244d9..810e44c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-themeablebrowser", - "version": "0.2.17", + "version": "0.2.17-custom", "description": "Cordova ThemeableBrowser Plugin", "cordova": { "id": "cordova-plugin-themeablebrowser", @@ -18,7 +18,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/initialxy/cordova-plugin-themeablebrowser" + "url": "git+https://github.com/initialxy/cordova-plugin-themeablebrowser.git" }, "keywords": [ "cordova", @@ -36,6 +36,19 @@ "version": ">=3.1.0" } ], - "author": "Apache Software Foundation", - "license": "Apache 2.0" + "author": { + "name": "Apache Software Foundation" + }, + "license": "Apache 2.0", + "gitHead": "683111632d2c27b00713d708c3bdf49bf5d4783a", + "readme": "\n\ncordova-plugin-themeablebrowser\n===============================\n\nThis plugin is a fork of [org.apache.cordova.inappbrowser](https://github.com/apache/cordova-plugin-inappbrowser). It attempts to retain most of the features of the InAppBrowser. In fact, for the full list of features inherited from InAppBrowser, please refer to [InAppBrowser's documentation](https://github.com/apache/cordova-plugin-inappbrowser/blob/master/README.md).\n\nThe purpose of this plugin is to provide an in-app-browser that can also be configured to match the theme of your app, in order to give it a more immersive look and feel for your app, as well as provide a more consistent look and feel across platforms.\n\nThis plugin launches an in-app web view on top the existing [CordovaWebView](https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/CordovaWebView.java) by calling `cordova.ThemeableBrowser.open()`.\n\n // Keep in mind that you must add your own images to native resource.\n // Images below are for sample only. They are not imported by this plugin.\n cordova.ThemeableBrowser.open('http://apache.org', '_blank', {\n statusbar: {\n color: '#ffffffff'\n },\n toolbar: {\n height: 44,\n color: '#f0f0f0ff'\n },\n title: {\n color: '#003264ff',\n showPageTitle: true\n },\n backButton: {\n image: 'back',\n imagePressed: 'back_pressed',\n align: 'left',\n event: 'backPressed'\n },\n forwardButton: {\n image: 'forward',\n imagePressed: 'forward_pressed',\n align: 'left',\n event: 'forwardPressed'\n },\n closeButton: {\n image: 'close',\n imagePressed: 'close_pressed',\n align: 'left',\n event: 'closePressed'\n },\n customButtons: [\n {\n image: 'share',\n imagePressed: 'share_pressed',\n align: 'right',\n event: 'sharePressed'\n }\n ],\n menu: {\n image: 'menu',\n imagePressed: 'menu_pressed',\n title: 'Test',\n cancel: 'Cancel',\n align: 'right',\n items: [\n {\n event: 'helloPressed',\n label: 'Hello World!'\n },\n {\n event: 'testPressed',\n label: 'Test!'\n }\n ]\n },\n backButtonCanClose: true\n }).addEventListener('backPressed', function(e) {\n alert('back pressed');\n }).addEventListener('helloPressed', function(e) {\n alert('hello pressed');\n }).addEventListener('sharePressed', function(e) {\n alert(e.url);\n }).addEventListener(cordova.ThemeableBrowser.EVT_ERR, function(e) {\n console.error(e.message);\n }).addEventListener(cordova.ThemeableBrowser.EVT_WRN, function(e) {\n console.log(e.message);\n });\n\n![iOS Sample](doc/images/ios_sample_01.png)\n![iOS Menu Sample](doc/images/ios_menu_sample_01.png)\n\n![Android Sample](doc/images/android_sample_01.png)\n![Android Menu Sample](doc/images/android_menu_sample_01.png)\n\nInstallation\n------------\n\n cordova plugin add cordova-plugin-themeablebrowser\n\nAdditional Properties\n---------------------\n\nIn addition to InAppBrowser's properties, following properties were added to fulfill this plugin's purpose in a nested JSON object.\n\n+ `statusbar` applicable to only iOS 7+.\n + `color` sets status bar color for iOS 7+ in RGBA web hex format. eg. `#fff0f0ff`. Default to white. Applicable to only iOS 7+.\n+ `toolbar`\n + `height` sets height of toolbar. Default to 44.\n + `color` sets browser toolbar color in RGBA web hex format. eg. `#fff0f0ff`. Default to white. Also see `image`.\n + `image` sets an image as browser toolbar background in titled mode. This property references to a **native** image resource, therefore it is platform dependent.\n+ `title`\n + `color` sets title text color in RGBA web hex format. eg. `#fff0f0ff`. Default to black.\n + `staticText` sets static text for title. This property overrides `showPageTitle` (see below).\n + `showPageTitle` when set to true, title of the current web page will be shown.\n+ `backButton`\n + `image` sets image for back button. This property references to a **native** image resource, therefore it is platform dependent.\n + `imagePressed` sets image for back button in its pressed state. This property references to a **native** image resource, therefore it is platform dependent.\n + `align` aligns back button to either `left` or `right`. Default to `left`.\n + `event` raises an custom event with given text as event name when back button is pressed. Optional.\n+ `forwardButton`\n + `image` sets image for forward button. This property references to a **native** image resource, therefore it is platform dependent.\n + `imagePressed` sets image for forward button in its pressed state. This property references to a **native** image resource, therefore it is platform dependent.\n + `align` aligns forward button to either `left` or `right`. Default to `left`.\n + `event` raises an custom event with given text as event name when forward button is pressed. Optional.\n+ `closeButton`\n + `image` sets image for close button. This property references to a **native** image resource, therefore it is platform dependent.\n + `imagePressed` sets image for close button in its pressed state. This property references to a **native** image resource, therefore it is platform dependent.\n + `align` aligns close button to either `left` or `right`. Default to `left`.\n + `event` raises an custom event with given text as event name when close button is pressed. Optional.\n+ `menu`\n + `title` sets menu title when menu button is clicked. iOS only.\n + `cancel` sets menu cancel button text. iOS only.\n + `image` sets image for menu button. This property references to a **native** image resource, therefore it is platform dependent.\n + `imagePressed` sets image for menu button in its pressed state. This property references to a **native** image resource, therefore it is platform dependent.\n + `event` raises an custom event with given text as event name when menu button is pressed. Optional.\n + `align` aligns menu button to either `left` or `right`. Default to `left`.\n + `items` is a list of items to be shown when menu is open\n + `event` defines the event name that will be raised when this menu item is clicked. The callbacks to menu events will receive an event object that contains the following properties: `url` is the current URL shown in browser and `index` is the index of the selected item in `items`.\n + `label` defines the menu item label text.\n+ `customButtons` is a list of objects that will be inserted into toolbar when given.\n + `image` sets image for custom button. This property references to a **native** image resource, therefore it is platform dependent.\n + `imagePressed` sets image for custom button in its pressed state. This property references to a **native** image resource, therefore it is platform dependent.\n + `align` aligns custom button to either `left` or `right`. Default to `left`.\n + `event` raises an custom event with given text as event name when custom button is pressed. The callbacks to custom button events will receive an event object that contains the following properties: `url` is the current URL shown in browser and `index` is the index of the selected button in `customButtons`.\n+ `backButtonCanClose` allows back button to close browser when there's no more to go back. Otherwise, back button will be disabled.\n+ `disableAnimation` when set to true, disables browser show and close animations.\n+ `fullscreen` when set to `true`, WebView will expand to the full height of the app, going under the toolbar. This flag combined with transparent toolbar color could allow toolbar buttons to appear floating on top of the WebView. (Remember, this plugin supports RGBA color format.) Optional.\n\nAll properties are optional with little default values. If a property is not given, its corresponding UI element will not be shown.\n\nOne thing to note is that all image resources reference to **native** resource bundle. So all images need to be imported to native project first. In case of Android, the image name will be looked up under `R.drawable`. eg. If image name is `hello_world`, `R.drawable.hello_world` will be referenced.\n\nYou may have noticed that ThemedBrowser added an optional menu as well as custom buttons, which you can utilize to respond to some simple user actions.\n\nExperimental Properties\n-----------------------\n\nFollowings are experimental properties that can be used in some special cases. Usage of these property are discouraged due to stability and efficiency.\n\nFor any object that supports `image` and `imagePressed` properties, there is a set of fallback properties that can be used when you absolutely cannot import native sources due to some circumstances.\n\n+ `(\\w+Button|menu|toolbar)`\n + `wwwImage` is like `image` but loads image from Cordova's `www` directory instead. This is a fallback solution when you cannot import native resources. Use `image` property as much as possible.\n + `wwwImagePressed` is like `image` but loads image from Cordova's `www` directory instead. This is a fallback solution when you cannot import native resources. Use `image` property as much as possible.\n + `wwwImageDensity` is needed when `wwwImage` and/or `wwwImagePressed` are given. Since these images are not loaded from resource bundle, density is unknown, therefore density needs to set by this property. Corresponds to iOS' `@2x`, `@3x` suffix.\n\neg.\n\n cordova.ThemeableBrowser.open('http://apache.org', '_blank', {\n ...\n backButton: {\n wwwImage: 'images/back.png',\n wwwImagePressed: 'images/back_pressed.png',\n wwwImageDensity: 2,\n align: 'left',\n event: 'backPressed'\n }\n ...\n });\n\nFile path is relative to `www` directory, which contains your web app sources. One thing that is very important is the `wwwImageDensity` property. Since images are not loaded from native resource bundle, density of any loaded images cannot not be automatically determined, therefore it needs to be explicity set. *You* are responsible for supplying the correct images with its corresponding density for any given device. If you don't know what image density means, please read [this documentation](https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/SupportingHiResScreensInViews/SupportingHiResScreensInViews.html). Ideally you are supposed to perform a device detection based on `window.devicePixelRatio` to supply optimal images. However a cheap way is to always supply high density images and rely on OS to scale down for lower screen density devices. Of course this would be inefficient, but it would save you a lot of trouble. Following is a cheatsheet that corresponds `wwwImageDensity` values to iOS and Android densities. Though `wwwImageDensity` does accept float values, followings are handy lookup.\n\n| `wwwImageDensity` | iOS | Android |\n| ----------------- | ------------ | ------- |\n| 1 | *No suffix* | mdpi |\n| 2 | `@2x` | xhdpi |\n| 3 | `@3x` | xxdpi |\n| 4 | *N/A* | xxxhdpi |\n\nAdditional Methods\n------------------\n\nThe reference object returned by `cordova.ThemeableBrowser.open` contains the following methods in addition to InAppBrowser's implementation:\n\n+ `reload` reloads the current page.\n\nErrors and Warnings\n-------------------\n\nThis plugin does not want to be the source of your app crash, not to mention that you have no way to catch exceptions from native code, so it does not throw exceptions. Neither does it want to write to log, because it wants to avoid polluting your log and respect your choice of logging library. Hence all errors are warnings are reported back to you through events. You may listen to two special events defined by `cordova.ThemeableBrowser.EVT_ERR` and `cordova.ThemeableBrowser.EVT_WRN`. Upon error or warning, you will receive event object that contains the following properties:\n\n+ `code` contains the error or warning code, which is defined by one of the followings:\n + `cordova.ThemeableBrowser.ERR_CRITICAL` is raised for a critical error that you should definitely try to resolve. eg. JSON parser failure. Dialer launch failure. Raised only for `cordova.ThemeableBrowser.EVT_ERR` event.\n + `cordova.ThemeableBrowser.ERR_LOADFAIL` is raised when a native image that you referenced in your config failed to load from native resource bundle. Raised only for `cordova.ThemeableBrowser.EVT_ERR` event.\n + `cordova.ThemeableBrowser.WRN_UNDEFINED` is raised when a property in your config is not defined. You will not get this warning for every property that is undefined, just the ones that might cause confusion. Raised only for `cordova.ThemeableBrowser.EVT_WRN` event.\n + `cordova.ThemeableBrowser.WRN_UNEXPECTED` is raised when an unexpected behaviour is committed. You can ignore this warning, since such behaviours will be simply ignored. eg. Try to close the browser when it's already closed. Raised only for `cordova.ThemeableBrowser.EVT_WRN` event.\n+ `message` contains a readable message that will try its best to tell you want went wrong.\n\nExamples:\n\n cordova.ThemeableBrowser.open('http://apache.org', '_blank', {\n ...\n }).addEventListener(cordova.ThemeableBrowser.EVT_ERR, function(e) {\n if (e.code === cordova.ThemeableBrowser.ERR_CRITICAL) {\n // TODO: Handle critical error.\n } else if (e.code === cordova.ThemeableBrowser.ERR_LOADFAIL) {\n // TODO: Image failed to load.\n }\n\n console.error(e.message);\n }).addEventListener(cordova.ThemeableBrowser.EVT_WRN, function(e) {\n if (e.code === cordova.ThemeableBrowser.WRN_UNDEFINED) {\n // TODO: Some property undefined in config.\n } else if (e.code === cordova.ThemeableBrowser.WRN_UNEXPECTED) {\n // TODO: Something strange happened. But no big deal.\n }\n\n console.log(e.message);\n });\n\nThese events are intended to help you debug strange behaviours. So if you run into something weird, please listene to these events and it might just tell you what's wrong. Please note errors and warnings are not completely consistent across platforms. There are some minor platform differences.\n\nImport Native Images\n--------------------\n\nIf you are a native developer and are already aware how to import native image resources, feel free to skip this section. Otherwise, here are some tips. First of all, your native iOS and Android projects are located at:\n\n /platforms/ios\n /platforms/android\n\nLet's start with Android, which is quite straightforward. Prepare your images for all of the pixel densities that you'd like to support. [Here is a documentation](http://developer.android.com/guide/practices/screens_support.html) that explains this concept. The gist is that on higher pixel density screens, your images will have to have higher resolution in order to look sharp on an actual device, so you want to prepare multiple files for the same image at different resolutions for their respective pixel density. In Android, there are a lot of densities due to diversity of devices, so you have to decide which ones you want to support. Fortunately if you don't have an image for a particular pixel density, Android will automatically pick up the closest one and try to down scale or up scale it. Of course this process is not very efficient, so you have to make your decisions. The directory where you want to place your images are under\n\n /platforms/android/res\n\nNotice how there are multiple folders named `drawble-.*`. Each file for the same image should be named the same, but it will need to be moved under the correct directory with respect to its target density. eg. If `icon.png` is intended for xhdpi, then it needs to go under `drawable-xhdpi` directory. In your JavaScript config, you can then reference to this iamge without extension. eg. With the previous example, simply `icon` will suffice.\n\nTo import image resources for iOS, it is slightly trickier, because you **have** to register your file in Xcode project file with help from Xcode, and there are two ways of doing this. Let's start with the old school way. iOS also shares similar concept with Android in terms of pixel density. iPhone to iPhone 3GS uses 1x the resolution, iPhone 4 to iPhone 6 uses 2x the resolution while iPhone 6 Plus and above uses 3x the resolution (even though it's actually down scaled, but that's a different discussion). In the old school way, you have to name your images with `@1x`, `@2x`, and `@3x` suffix with respect to their target density. eg. `icon@2x.png`. [Here is a documentation](https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/SupportingHiResScreensInViews/SupportingHiResScreensInViews.html) that explains this concept. You then have to move it under\n\n /platforms/ios//Resources\n\nThen open your native iOS project with Xcode by double clicking on\n\n /platforms/ios/.xcodeproj\n\nIn the left hand side panel, make sure you are in Project navigator tab. Then you can see a list of directories under your project. One of them being `Resources`, but you don't see your newly added images there. Now you need to drag your images fron Finder to Xcode and drop it under `Resource` folder. In your JavaScript config, you can then reference to them without suffix or extension. eg. With the previous example, simply `icon` will suffice.\n\nThe new school way is to use [Asset Catalog](https://developer.apple.com/library/ios/recipes/xcode_help-image_catalog-1.0/Recipe.html). This is the recommended technique from Xcode 5+ and iOS 7+. It gives you better management of all of your image resources. ie. No more suffix, and you can see all your images for different densities in one table etc. However there are more steps involved to set it up. Please reference to [this guide](http://www.intertech.com/Blog/xcode-assets-xcassets/) for a step by step walkthrough.\n\nIf for some reason you absolutely cannot import native images, you may consider using the `wwwImage`, `wwwImagePressed` and `wwwImageDensity` properties as fallback solution, though this is an experimental feature and is discouraged. See [above](#experimental-properties) for documentation.\n\nFAQ\n---\n\n### I just installed this plugin, how come it just shows a blank toolbar?\n\nThe purpose of this plugin is to allow **you** to style the in app browser the way you want. Isn't that why you installed this plugin in the first place? Hence, it does not come with any defaults. Every UI element needs to be styled by you, otherwise it's hidden. This also avoids polluting your resouce bundle with default images.\n\n### Why does my menu on Android look ugly?\n\nAndroid menu is simply a [Spinner](http://developer.android.com/guide/topics/ui/controls/spinner.html), which picks up its style from your Activity's theme. By default Cordova uses the very old [Theme.Black.NoTitleBar](http://developer.android.com/reference/android/R.style.html#Theme_Black_NoTitleBar), which is ugly. Open your AndroidManifest.xml and change your `android:theme` attribute to something more morden, such as [Theme.Holo](http://developer.android.com/reference/android/R.style.html#Theme_Holo) or [Base.Theme.AppCompat](http://developer.android.com/reference/android/support/v7/appcompat/R.style.html#Base_Theme_AppCompat) from [support library](https://developer.android.com/tools/support-library/features.html#v7-appcompat).\n\n### How do I style Android menu?\n\nAndroid menu is simply a [Spinner](http://developer.android.com/guide/topics/ui/controls/spinner.html) with default layout resources, which picks up its style from your Activity's theme. You can style it by making a theme of your app and apply it to your activity. See `android:dropDownListViewStyle`.\n\n### How do I add margings and paddings?\n\nThere is no margins or paddings. However notice that you can assign images to each of the buttons. So take advantage of PNG's transparency to create margins/paddings around your buttons.\n\n### How do I add shadow to the toolbar?\n\nFirst, notice that you can use an image as well as color for toolbar background. Use PNG for background image and create shadow inside this image. Next, you will probably be concerned about how buttons will slightly misaligned due since they always middle align. Again create some transparent borders in your button images to offset the misalignment. eg. Say your shadow is 5px tall, which causes buttons to allear lower than they shoud. Create a 10px transparent bottom border for each of your button icons and you are set.\n\nSupported Platforms\n-------------------\n\n+ iOS 5.0+\n+ Android 2.0+\n\nCurrently there is no plan to support other platforms, though source code from InAppBrowser is kept for merge purposes, they are inactive, since they are removed from `plugin.xml`.\n\nMigration\n---------\n\nThis plugin is **not** a drop-in replacement for InAppBrowser. The biggest change that was made from InAppBrowser, which caused it to be no longer compatible with InAppBrowser's API is that `options` parameter now accepts a JavaScript object instead of string.\n\n cordova.ThemeableBrowser.open('http://apache.org', '_blank', {\n customButtons: [\n {\n image: 'share',\n imagePressed: 'share_pressed',\n align: 'right',\n event: 'sharePressed'\n }\n ],\n menu: {\n image: 'menu',\n imagePressed: 'menu_pressed',\n items: [\n {\n event: 'helloPressed',\n label: 'Hello World!'\n },\n {\n event: 'testPressed',\n label: 'Test!'\n }\n ]\n }\n });\n\nAs you can see from above, this allows configurations to have more robust and readable definition.\n\nFurthermore, the object returned by `open` always returns its own instance allowing chaining of methods. Obviously, this breaks the immitation of `window.open()`, however it's an optional feature that you can choose not to use if you want to stay loyal to the original.\n\n cordova.ThemeableBrowser.open('http://apache.org', '_blank', {\n customButtons: [\n {\n image: 'share',\n imagePressed: 'share_pressed',\n align: 'right',\n event: 'sharePressed'\n }\n ],\n menu: {\n image: 'menu',\n imagePressed: 'menu_pressed',\n items: [\n {\n event: 'helloPressed',\n label: 'Hello World!'\n },\n {\n event: 'testPressed',\n label: 'Test!'\n }\n ]\n }\n }).addEventListener('sharePressed', function(event) {\n alert(event.url);\n }).addEventListener('helloPressed', function(event) {\n alert(event.url);\n }).addEventListener('testPressed', function(event) {\n alert(event.url);\n });\n\nTwo properties from InAppBrowser are disabled.\n+ `location` is always `false` because address bar is not needed for an immersive experience of an integrated browser.\n+ `toolbarposition` is always `top` to remain consistent across platforms.\n\nOne is redefined.\n+ `toolbar` is redefined to contain toolbar settings and toolbar is always shown, because the whole point why you are using this plugin is to style toolbar right?\n\nLicense\n-------\n\nThis project is licensed under Aapache License 2.0. See [LICENSE](LICENSE) file.", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/initialxy/cordova-plugin-themeablebrowser/issues" + }, + "homepage": "https://github.com/initialxy/cordova-plugin-themeablebrowser#readme", + "_id": "cordova-plugin-themeablebrowser@0.2.17", + "_shasum": "38dbe1326c1d52ec83a6f7c7bbfc66c01d7be0f9", + "_from": "git+https://github.com/dpa99c/cordova-plugin-themeablebrowser.git", + "_resolved": "git+https://github.com/dpa99c/cordova-plugin-themeablebrowser.git#683111632d2c27b00713d708c3bdf49bf5d4783a" } diff --git a/plugin.xml b/plugin.xml index 9c4392d9a..5f115f51f 100644 --- a/plugin.xml +++ b/plugin.xml @@ -21,7 +21,7 @@ + version="0.2.17-custom"> ThemeableBrowser Cordova ThemeableBrowser Plugin @@ -49,6 +49,8 @@ + + @@ -64,6 +66,8 @@ + + diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 673e0d828..c31990ec9 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -49,6 +49,9 @@ Licensed to the Apache Software Foundation (ASF) under one import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.webkit.CookieManager; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -66,9 +69,9 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.cordova.CordovaArgs; 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.apache.cordova.Whitelist; import org.json.JSONException; import org.json.JSONObject; @@ -77,6 +80,7 @@ Licensed to the Apache Software Foundation (ASF) under one import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Arrays; @SuppressLint("SetJavaScriptEnabled") public class ThemeableBrowser extends CordovaPlugin { @@ -90,6 +94,7 @@ public class ThemeableBrowser extends CordovaPlugin { 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 ALIGN_LEFT = "left"; private static final String ALIGN_RIGHT = "right"; @@ -109,6 +114,11 @@ public class ThemeableBrowser extends CordovaPlugin { private EditText edittext; private CallbackContext callbackContext; + private ValueCallback mUploadCallback; + private ValueCallback mUploadCallbackLollipop; + private final static int FILECHOOSER_REQUESTCODE = 1; + private final static int FILECHOOSER_REQUESTCODE_LOLLIPOP = 2; + /** * Executes the request and returns PluginResult. * @@ -396,14 +406,17 @@ public void run() { // NB: wait for about:blank before dismissing public void onPageFinished(WebView view, String url) { if (dialog != null) { - dialog.dismiss(); + try{ + dialog.dismiss(); + + dialog = null; + inAppWebView = null; + edittext = null; + callbackContext = null; + }catch (Exception e){ + Log.e(LOG_TAG, "Error dismissing dialog: "+e.getMessage()); + } } - - // Clean up. - dialog = null; - inAppWebView = null; - edittext = null; - callbackContext = null; } }); @@ -558,10 +571,12 @@ public void run() { toolbar.setBackgroundColor(hexStringToColor( toolbarDef != null && toolbarDef.color != null ? toolbarDef.color : "#ffffffff")); - toolbar.setLayoutParams(new ViewGroup.LayoutParams( + + ViewGroup.LayoutParams toolbarLayoutParams = new ViewGroup.LayoutParams( LayoutParams.MATCH_PARENT, dpToPixels(toolbarDef != null - ? toolbarDef.height : TOOLBAR_DEF_HEIGHT))); + ? toolbarDef.height : TOOLBAR_DEF_HEIGHT)); + toolbar.setLayoutParams(toolbarLayoutParams); if (toolbarDef != null && (toolbarDef.image != null || toolbarDef.wwwImage != null)) { @@ -619,7 +634,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { // Back button final Button back = createButton( features.backButton, - "back button", + "back button" , new View.OnClickListener() { public void onClick(View v) { emitButtonEvent( @@ -642,7 +657,7 @@ public void onClick(View v) { // Forward button final Button forward = createButton( features.forwardButton, - "forward button", + "forward button" , new View.OnClickListener() { public void onClick(View v) { emitButtonEvent( @@ -662,7 +677,7 @@ public void onClick(View v) { // Close/Done button Button close = createButton( features.closeButton, - "close button", + "close button" , new View.OnClickListener() { public void onClick(View v) { emitButtonEvent( @@ -673,13 +688,19 @@ public void onClick(View v) { } ); + // Menu button Spinner menu = features.menu != null ? new MenuSpinner(cordova.getActivity()) : null; if (menu != null) { menu.setLayoutParams(new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - menu.setContentDescription("menu button"); + + if(features.menu.accessibilityDescription != null){ + menu.setContentDescription(features.menu.accessibilityDescription); + }else{ + menu.setContentDescription("menu button"); + } setButtonImages(menu, features.menu, DISABLED_ALPHA); // We are not allowed to use onClickListener for Spinner, so we will use @@ -697,6 +718,13 @@ public boolean onTouch(View v, MotionEvent event) { }); if (features.menu.items != null) { + if (features.menu.cancel != null) { + EventLabel cancelEventLabel = new EventLabel(); + cancelEventLabel.label = features.menu.cancel; + cancelEventLabel.event = "cancel"; + features.menu.items = ArrayHelper.push(features.menu.items, cancelEventLabel); + } + HideSelectedAdapter adapter = new HideSelectedAdapter( cordova.getActivity(), @@ -728,6 +756,7 @@ public void onNothingSelected( } } + // Title final TextView title = features.title != null ? new TextView(cordova.getActivity()) : null; @@ -746,6 +775,9 @@ public void onNothingSelected( if (features.title.staticText != null) { title.setText(features.title.staticText); } + if (features.title.fontSize != null) { + title.setTextSize(features.title.fontSize); + } } // WebView @@ -757,7 +789,51 @@ public void onNothingSelected( ((LinearLayout.LayoutParams) inAppWebViewParams).weight = 1; } inAppWebView.setLayoutParams(inAppWebViewParams); - inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView)); + // File Chooser Implemented ChromeClient + inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView) { + // For Android 5.0 + public boolean onShowFileChooser (WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) + { + LOG.d(LOG_TAG, "File Chooser 5.0 "); + // If callback exists, finish it. + if(mUploadCallbackLollipop != null) { + mUploadCallbackLollipop.onReceiveValue(null); + } + mUploadCallbackLollipop = 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(ThemeableBrowser.this, Intent.createChooser(content, "Select File"), FILECHOOSER_REQUESTCODE_LOLLIPOP); + return true; + } + + // For Android 4.1 + public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) + { + LOG.d(LOG_TAG, "File Chooser 4.1 "); + // Call file chooser for Android 3.0 + openFileChooser(uploadMsg, acceptType); + } + + // For Android 3.0 + public void openFileChooser(ValueCallback uploadMsg, String acceptType) + { + LOG.d(LOG_TAG, "File Chooser 3.0 "); + mUploadCallback = uploadMsg; + Intent content = new Intent(Intent.ACTION_GET_CONTENT); + content.addCategory(Intent.CATEGORY_OPENABLE); + + // run startActivityForResult + cordova.startActivityForResult(ThemeableBrowser.this, Intent.createChooser(content, "Select File"), FILECHOOSER_REQUESTCODE); + } + + }); + + WebViewClient client = new ThemeableBrowserClient(thatWebView, new PageLoadListener() { @Override public void onPageFinished(String url, boolean canGoBack, boolean canGoForward) { @@ -785,6 +861,24 @@ public void onPageFinished(String url, boolean canGoBack, boolean canGoForward) settings.setDisplayZoomControls(false); settings.setPluginState(android.webkit.WebSettings.PluginState.ON); + // Add JS 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."); + } + } + } + if (Build.VERSION.SDK_INT >= 17){ + inAppWebView.addJavascriptInterface(new JsObject(), "cordova_iab"); + } + //Toggle whether this is enabled or not! Bundle appSettings = cordova.getActivity().getIntent().getExtras(); boolean enableDatabase = appSettings == null || appSettings.getBoolean("ThemeableBrowserStorageEnabled", true); @@ -910,10 +1004,30 @@ public void onClick(View view) { int titleMargin = Math.max( leftContainerWidth, rightContainerWidth); + int paddingX = features.toolbar.paddingX; + int titleMarginLeft, titleMarginRight; + titleMarginLeft = titleMarginRight = titleMargin; + if (leftContainerWidth == 0){ + titleMarginLeft = paddingX; + title.setGravity(Gravity.LEFT); + }else if (rightContainerWidth == 0){ + titleMarginRight = paddingX; + title.setGravity(Gravity.RIGHT); + } + FrameLayout.LayoutParams titleParams = (FrameLayout.LayoutParams) title.getLayoutParams(); - titleParams.setMargins(titleMargin, 0, titleMargin, 0); - toolbar.addView(title); + titleParams.setMargins(titleMarginLeft, 0, titleMarginRight, 0); + + ViewGroup titleContainer; + if (leftContainerWidth == 0){ + titleContainer = leftButtonContainer; + }else if (rightContainerWidth == 0){ + titleContainer = rightButtonContainer; + }else{ + titleContainer = toolbar; + } + titleContainer.addView(title); } if (features.fullscreen) { @@ -940,6 +1054,7 @@ public void onClick(View view) { 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(features.hidden) { @@ -1133,12 +1248,12 @@ private void setBackground(View view, Drawable drawable) { } } - private Button createButton(BrowserButton buttonProps, String description, + private Button createButton(BrowserButton buttonProps, String defaultDescription, View.OnClickListener listener) { Button result = null; if (buttonProps != null) { result = new Button(cordova.getActivity()); - result.setContentDescription(description); + result.setContentDescription(buttonProps.accessibilityDescription != null ? buttonProps.accessibilityDescription : defaultDescription); result.setLayoutParams(new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); setButtonImages(result, buttonProps, DISABLED_ALPHA); @@ -1148,7 +1263,7 @@ private Button createButton(BrowserButton buttonProps, String description, } else { emitWarning(WRN_UNDEFINED, String.format("%s is not defined. Button will not be shown.", - description)); + defaultDescription)); } return result; } @@ -1184,6 +1299,42 @@ public void onPageFinished(String url, boolean canGoBack, boolean canGoForward); } + /** + * 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) { + // For Android >= 5.0 + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + LOG.d(LOG_TAG, "onActivityResult (For Android >= 5.0)"); + // If RequestCode or Callback is Invalid + if(requestCode != FILECHOOSER_REQUESTCODE_LOLLIPOP || mUploadCallbackLollipop == null) { + super.onActivityResult(requestCode, resultCode, intent); + return; + } + mUploadCallbackLollipop.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent)); + mUploadCallbackLollipop = null; + } + // For Android < 5.0 + else { + LOG.d(LOG_TAG, "onActivityResult (For Android < 5.0)"); + // If RequestCode or Callback is Invalid + if(requestCode != FILECHOOSER_REQUESTCODE || mUploadCallback == null) { + super.onActivityResult(requestCode, resultCode, intent); + return; + } + + if (null == mUploadCallback) return; + Uri result = intent == null || resultCode != cordova.getActivity().RESULT_OK ? null : intent.getData(); + + mUploadCallback.onReceiveValue(result); + mUploadCallback = null; + } + } + /** * The webview client receives notifications about appView */ @@ -1307,6 +1458,11 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); + // Alias the iOS webkit namespace for postMessage() + if (Build.VERSION.SDK_INT >= 17){ + injectDeferredObject("window.webkit={messageHandlers:{cordova_iab:cordova_iab}}", null); + } + try { JSONObject obj = new JSONObject(); obj.put("type", LOAD_STOP_EVENT); @@ -1425,10 +1581,12 @@ private static class BrowserButton extends Event { public String wwwImagePressed; public double wwwImageDensity = 1; public String align = ALIGN_LEFT; + public String accessibilityDescription; } private static class BrowserMenu extends BrowserButton { public EventLabel[] items; + public String cancel; } private static class Toolbar { @@ -1437,11 +1595,26 @@ private static class Toolbar { public String image; public String wwwImage; public double wwwImageDensity = 1; + public int paddingX = 0; } private static class Title { public String color; public String staticText; public boolean showPageTitle; + public Float fontSize; + } + + public static class ArrayHelper { + public static T[] push(T[] arr, T item) { + T[] tmp = Arrays.copyOf(arr, arr.length + 1); + tmp[tmp.length - 1] = item; + return tmp; + } + + public static T[] pop(T[] arr) { + T[] tmp = Arrays.copyOf(arr, arr.length - 1); + return tmp; + } } } diff --git a/src/android/Whitelist.java b/src/android/Whitelist.java new file mode 100644 index 000000000..ee03fbcfa --- /dev/null +++ b/src/android/Whitelist.java @@ -0,0 +1,170 @@ +/* + 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 com.initialxy.cordova.themeablebrowser; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.cordova.LOG; + +import android.net.Uri; + +public class Whitelist { + private static class URLPattern { + public Pattern scheme; + public Pattern host; + public Integer port; + public Pattern path; + + private String regexFromPattern(String pattern, boolean allowWildcards) { + final String toReplace = "\\.[]{}()^$?+|"; + StringBuilder regex = new StringBuilder(); + for (int i=0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '*' && allowWildcards) { + regex.append("."); + } else if (toReplace.indexOf(c) > -1) { + regex.append('\\'); + } + regex.append(c); + } + return regex.toString(); + } + + public URLPattern(String scheme, String host, String port, String path) throws MalformedURLException { + try { + if (scheme == null || "*".equals(scheme)) { + this.scheme = null; + } else { + this.scheme = Pattern.compile(regexFromPattern(scheme, false), Pattern.CASE_INSENSITIVE); + } + if ("*".equals(host)) { + this.host = null; + } else if (host.startsWith("*.")) { + this.host = Pattern.compile("([a-z0-9.-]*\\.)?" + regexFromPattern(host.substring(2), false), Pattern.CASE_INSENSITIVE); + } else { + this.host = Pattern.compile(regexFromPattern(host, false), Pattern.CASE_INSENSITIVE); + } + if (port == null || "*".equals(port)) { + this.port = null; + } else { + this.port = Integer.parseInt(port,10); + } + if (path == null || "/*".equals(path)) { + this.path = null; + } else { + this.path = Pattern.compile(regexFromPattern(path, true)); + } + } catch (NumberFormatException e) { + throw new MalformedURLException("Port must be a number"); + } + } + + public boolean matches(Uri uri) { + try { + return ((scheme == null || scheme.matcher(uri.getScheme()).matches()) && + (host == null || host.matcher(uri.getHost()).matches()) && + (port == null || port.equals(uri.getPort())) && + (path == null || path.matcher(uri.getPath()).matches())); + } catch (Exception e) { + LOG.d(TAG, e.toString()); + return false; + } + } + } + + private ArrayList whiteList; + + public static final String TAG = "Whitelist"; + + public Whitelist() { + this.whiteList = new ArrayList(); + } + + /* Match patterns (from http://developer.chrome.com/extensions/match_patterns.html) + * + * := :// + * := '*' | 'http' | 'https' | 'file' | 'ftp' | 'chrome-extension' + * := '*' | '*.' + + * := '/' + * + * We extend this to explicitly allow a port attached to the host, and we allow + * the scheme to be omitted for backwards compatibility. (Also host is not required + * to begin with a "*" or "*.".) + */ + public void addWhiteListEntry(String origin, boolean subdomains) { + if (whiteList != null) { + try { + // Unlimited access to network resources + if (origin.compareTo("*") == 0) { + LOG.d(TAG, "Unlimited access to network resources"); + whiteList = null; + } + else { // specific access + Pattern parts = Pattern.compile("^((\\*|[A-Za-z-]+):(//)?)?(\\*|((\\*\\.)?[^*/:]+))?(:(\\d+))?(/.*)?"); + Matcher m = parts.matcher(origin); + if (m.matches()) { + String scheme = m.group(2); + String host = m.group(4); + // Special case for two urls which are allowed to have empty hosts + if (("file".equals(scheme) || "content".equals(scheme)) && host == null) host = "*"; + String port = m.group(8); + String path = m.group(9); + if (scheme == null) { + // XXX making it stupid friendly for people who forget to include protocol/SSL + whiteList.add(new URLPattern("http", host, port, path)); + whiteList.add(new URLPattern("https", host, port, path)); + } else { + whiteList.add(new URLPattern(scheme, host, port, path)); + } + } + } + } catch (Exception e) { + LOG.d(TAG, "Failed to add origin %s", origin); + } + } + } + + + /** + * Determine if URL is in approved list of URLs to load. + * + * @param uri + * @return true if wide open or whitelisted + */ + public boolean isUrlWhiteListed(String uri) { + // If there is no whitelist, then it's wide open + if (whiteList == null) return true; + + Uri parsedUri = Uri.parse(uri); + // Look for match in white list + Iterator pit = whiteList.iterator(); + while (pit.hasNext()) { + URLPattern p = pit.next(); + if (p.matches(parsedUri)) { + return true; + } + } + return false; + } + +} diff --git a/src/android/WhitelistPlugin.java b/src/android/WhitelistPlugin.java new file mode 100644 index 000000000..9972b4a11 --- /dev/null +++ b/src/android/WhitelistPlugin.java @@ -0,0 +1,160 @@ +/* + 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 com.initialxy.cordova.themeablebrowser; + +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.ConfigXmlParser; +import org.apache.cordova.LOG; +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; + +public class WhitelistPlugin extends CordovaPlugin { + private static final String LOG_TAG = "WhitelistPlugin"; + private Whitelist allowedNavigations; + private Whitelist allowedIntents; + private Whitelist allowedRequests; + + // Used when instantiated via reflection by PluginManager + public WhitelistPlugin() { + } + // These can be used by embedders to allow Java-configuration of whitelists. + public WhitelistPlugin(Context context) { + this(new Whitelist(), new Whitelist(), null); + new CustomConfigXmlParser().parse(context); + } + public WhitelistPlugin(XmlPullParser xmlParser) { + this(new Whitelist(), new Whitelist(), null); + new CustomConfigXmlParser().parse(xmlParser); + } + public WhitelistPlugin(Whitelist allowedNavigations, Whitelist allowedIntents, Whitelist allowedRequests) { + if (allowedRequests == null) { + allowedRequests = new Whitelist(); + allowedRequests.addWhiteListEntry("file:///*", false); + allowedRequests.addWhiteListEntry("data:*", false); + } + this.allowedNavigations = allowedNavigations; + this.allowedIntents = allowedIntents; + this.allowedRequests = allowedRequests; + } + @Override + public void pluginInitialize() { + if (allowedNavigations == null) { + allowedNavigations = new Whitelist(); + allowedIntents = new Whitelist(); + allowedRequests = new Whitelist(); + new CustomConfigXmlParser().parse(webView.getContext()); + } + } + + private class CustomConfigXmlParser extends ConfigXmlParser { + @Override + public void handleStartTag(XmlPullParser xml) { + String strNode = xml.getName(); + if (strNode.equals("content")) { + String startPage = xml.getAttributeValue(null, "src"); + allowedNavigations.addWhiteListEntry(startPage, false); + } else if (strNode.equals("allow-navigation")) { + String origin = xml.getAttributeValue(null, "href"); + if ("*".equals(origin)) { + allowedNavigations.addWhiteListEntry("http://*/*", false); + allowedNavigations.addWhiteListEntry("https://*/*", false); + allowedNavigations.addWhiteListEntry("data:*", false); + } else { + allowedNavigations.addWhiteListEntry(origin, false); + } + } else if (strNode.equals("allow-intent")) { + String origin = xml.getAttributeValue(null, "href"); + allowedIntents.addWhiteListEntry(origin, false); + } else if (strNode.equals("access")) { + String origin = xml.getAttributeValue(null, "origin"); + String subdomains = xml.getAttributeValue(null, "subdomains"); + boolean external = (xml.getAttributeValue(null, "launch-external") != null); + if (origin != null) { + if (external) { + LOG.w(LOG_TAG, "Found within config.xml. Please use instead."); + allowedIntents.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0)); + } else { + if ("*".equals(origin)) { + allowedRequests.addWhiteListEntry("http://*/*", false); + allowedRequests.addWhiteListEntry("https://*/*", false); + } else { + allowedRequests.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0)); + } + } + } + } + } + @Override + public void handleEndTag(XmlPullParser xml) { + } + } + + @Override + public Boolean shouldAllowNavigation(String url) { + if (allowedNavigations.isUrlWhiteListed(url)) { + return true; + } + return null; // Default policy + } + + @Override + public Boolean shouldAllowRequest(String url) { + if (Boolean.TRUE == shouldAllowNavigation(url)) { + return true; + } + if (allowedRequests.isUrlWhiteListed(url)) { + return true; + } + return null; // Default policy + } + + @Override + public Boolean shouldOpenExternalUrl(String url) { + if (allowedIntents.isUrlWhiteListed(url)) { + return true; + } + return null; // Default policy + } + + public Whitelist getAllowedNavigations() { + return allowedNavigations; + } + + public void setAllowedNavigations(Whitelist allowedNavigations) { + this.allowedNavigations = allowedNavigations; + } + + public Whitelist getAllowedIntents() { + return allowedIntents; + } + + public void setAllowedIntents(Whitelist allowedIntents) { + this.allowedIntents = allowedIntents; + } + + public Whitelist getAllowedRequests() { + return allowedRequests; + } + + public void setAllowedRequests(Whitelist allowedRequests) { + this.allowedRequests = allowedRequests; + } +} diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index ed59e22f1..a862c9a6e 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -20,12 +20,7 @@ #import #import #import - -#ifdef __CORDOVA_4_0_0 - #import -#else - #import -#endif +#import "CDVThemeableBrowserUIDelegate.h" @interface CDVThemeableBrowserOptions : NSObject {} @@ -63,9 +58,11 @@ @class CDVThemeableBrowserViewController; @interface CDVThemeableBrowser : CDVPlugin { + UIWindow * tmpWindow; BOOL _injectedIframeBridge; } +@property (nonatomic, retain) CDVThemeableBrowser* instance; @property (nonatomic, retain) CDVThemeableBrowserViewController* themeableBrowserViewController; @property (nonatomic, copy) NSString* callbackId; @property (nonatomic, copy) NSRegularExpression *callbackIdPattern; @@ -80,23 +77,16 @@ @end -@interface CDVThemeableBrowserViewController : UIViewController { +@interface CDVThemeableBrowserViewController : UIViewController { @private - NSString* _userAgent; - NSString* _prevUserAgent; - NSInteger _userAgentLockToken; UIStatusBarStyle _statusBarStyle; + CGFloat _initialStatusBarHeight; CDVThemeableBrowserOptions *_browserOptions; - -#ifdef __CORDOVA_4_0_0 - CDVUIWebViewDelegate* _webViewDelegate; -#else - CDVWebViewDelegate* _webViewDelegate; -#endif - + CGFloat _lastReducedStatusBarHeight; } -@property (nonatomic, strong) IBOutlet UIWebView* webView; +@property (nonatomic, strong) IBOutlet WKWebView* webView; +@property (nonatomic, strong) IBOutlet WKWebViewConfiguration* configuration; @property (nonatomic, strong) IBOutlet UIButton* closeButton; @property (nonatomic, strong) IBOutlet UILabel* addressLabel; @property (nonatomic, strong) IBOutlet UILabel* titleLabel; @@ -105,6 +95,7 @@ @property (nonatomic, strong) IBOutlet UIButton* menuButton; @property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; @property (nonatomic, strong) IBOutlet UIView* toolbar; +@property (nonatomic, strong) IBOutlet CDVThemeableBrowserUIDelegate* webViewUIDelegate; @property (nonatomic, strong) NSArray* leftButtons; @property (nonatomic, strong) NSArray* rightButtons; @@ -112,7 +103,9 @@ @property (nonatomic, weak) id orientationDelegate; @property (nonatomic, weak) CDVThemeableBrowser* navigationDelegate; @property (nonatomic) NSURL* currentURL; -@property (nonatomic) CGFloat titleOffset; +@property (nonatomic) CGFloat titleOffsetLeft; +@property (nonatomic) CGFloat titleOffsetRight; +@property (nonatomic) CGFloat toolbarPaddingX; - (void)close; - (void)reload; @@ -121,7 +114,7 @@ - (void)showToolBar:(BOOL)show : (NSString*) toolbarPosition; - (void)setCloseButtonTitle:(NSString*)title; -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle; +- (id)init:(CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle; + (UIColor *)colorFromRGBA:(NSString *)rgba; diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index ce09b20ce..76e5ef93d 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -6,9 +6,9 @@ Licensed to the Apache Software Foundation (ASF) under one 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 @@ -19,7 +19,10 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVThemeableBrowser.h" #import -#import + +#if __has_include("CDVWKProcessPoolFactory.h") +#import "CDVWKProcessPoolFactory.h" +#endif #define kThemeableBrowserTargetSelf @"_self" #define kThemeableBrowserTargetSystem @"_system" @@ -28,6 +31,8 @@ Licensed to the Apache Software Foundation (ASF) under one #define kThemeableBrowserToolbarBarPositionBottom @"bottom" #define kThemeableBrowserToolbarBarPositionTop @"top" +#define IAB_BRIDGE_NAME @"cordova_iab" + #define kThemeableBrowserAlignLeft @"left" #define kThemeableBrowserAlignRight @"right" @@ -44,8 +49,12 @@ Licensed to the Apache Software Foundation (ASF) under one #define kThemeableBrowserPropShowPageTitle @"showPageTitle" #define kThemeableBrowserPropAlign @"align" #define kThemeableBrowserPropTitle @"title" +#define kThemeableBrowserPropTitleFontSize @"fontSize" #define kThemeableBrowserPropCancel @"cancel" #define kThemeableBrowserPropItems @"items" +#define kThemeableBrowserPropAccessibilityDescription @"accessibilityDescription" +#define kThemeableBrowserPropStatusBarStyle @"style" +#define kThemeableBrowserPropToolbarPaddingX @"paddingX" #define kThemeableBrowserEmitError @"ThemeableBrowserError" #define kThemeableBrowserEmitWarning @"ThemeableBrowserWarning" @@ -54,7 +63,7 @@ Licensed to the Apache Software Foundation (ASF) under one #define kThemeableBrowserEmitCodeUnexpected @"unexpected" #define kThemeableBrowserEmitCodeUndefined @"undefined" -#define TOOLBAR_DEF_HEIGHT 44.0 +#define TOOLBAR_HEIGHT 44.0 #define LOCATIONBAR_HEIGHT 21.0 #define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) @@ -70,26 +79,13 @@ @interface CDVThemeableBrowser () { @implementation CDVThemeableBrowser -#ifdef __CORDOVA_4_0_0 - (void)pluginInitialize { _isShown = NO; _framesOpened = 0; _callbackIdPattern = nil; } -#else -- (CDVThemeableBrowser*)initWithWebView:(UIWebView*)theWebView -{ - self = [super initWithWebView:theWebView]; - if (self != nil) { - _isShown = NO; - _framesOpened = 0; - _callbackIdPattern = nil; - } - return self; -} -#endif - (void)onReset { @@ -109,29 +105,29 @@ - (void)close:(CDVInvokedUrlCommand*)command - (BOOL) isSystemUrl:(NSURL*)url { - NSDictionary *systemUrls = @{ - @"itunes.apple.com": @YES, - @"search.itunes.apple.com": @YES, - @"appsto.re": @YES - }; - - if (systemUrls[[url host]]) { - return YES; - } - - return NO; + NSDictionary *systemUrls = @{ + @"itunes.apple.com": @YES, + @"search.itunes.apple.com": @YES, + @"appsto.re": @YES + }; + + if (systemUrls[[url host]]) { + return YES; + } + + return NO; } - (void)open:(CDVInvokedUrlCommand*)command { CDVPluginResult* pluginResult; - + NSString* url = [command argumentAtIndex:0]; NSString* target = [command argumentAtIndex:1 withDefault:kThemeableBrowserTargetSelf]; NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; - + self.callbackId = command.callbackId; - + if (url != nil) { #ifdef __CORDOVA_4_0_0 NSURL* baseUrl = [self.webViewEngine URL]; @@ -139,13 +135,13 @@ - (void)open:(CDVInvokedUrlCommand*)command NSURL* baseUrl = [self.webView.request URL]; #endif NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; - + initUrl = absoluteUrl; - + if ([self isSystemUrl:absoluteUrl]) { target = kThemeableBrowserTargetSystem; } - + if ([target isEqualToString:kThemeableBrowserTargetSelf]) { [self openInCordovaWebView:absoluteUrl withOptions:options]; } else if ([target isEqualToString:kThemeableBrowserTargetSystem]) { @@ -153,12 +149,12 @@ - (void)open:(CDVInvokedUrlCommand*)command } else { // _blank or anything else [self openInThemeableBrowser: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]; } @@ -173,7 +169,7 @@ - (void)reload:(CDVInvokedUrlCommand*)command - (CDVThemeableBrowserOptions*)parseOptions:(NSString*)options { CDVThemeableBrowserOptions* obj = [[CDVThemeableBrowserOptions alloc] init]; - + if (options && [options length] > 0) { // Min support, iOS 5. We will use the JSON parser that comes with iOS // 5. @@ -183,7 +179,7 @@ - (CDVThemeableBrowserOptions*)parseOptions:(NSString*)options JSONObjectWithData:data options:0 error:&error]; - + if(error) { [self emitError:kThemeableBrowserEmitCodeCritical withMessage:[NSString stringWithFormat:@"Invalid JSON %@", error]]; @@ -197,58 +193,110 @@ - (CDVThemeableBrowserOptions*)parseOptions:(NSString*)options } } else { [self emitWarning:kThemeableBrowserEmitCodeUndefined - withMessage:@"No config was given, defaults will be used, which is quite boring."]; + withMessage:@"No config was given, defaults will be used, which is quite boring."]; } - + return obj; } - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options { CDVThemeableBrowserOptions* browserOptions = [self parseOptions:options]; - + // Among all the options, there are a few that ThemedBrowser would like to // disable, since ThemedBrowser's purpose is to provide an integrated look // and feel that is consistent across platforms. We'd do this hack to // minimize changes from the original ThemeableBrowser so when merge from the // ThemeableBrowser is needed, it wouldn't be super pain in the ass. browserOptions.toolbarposition = kThemeableBrowserToolbarBarPositionTop; - - if (browserOptions.clearcache) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"]) { - [storage deleteCookie:cookie]; + + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + + 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) { - NSHTTPCookie *cookie; - NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) - { - if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { - [storage deleteCookie:cookie]; + + 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"); + } + } + + UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; + if(browserOptions.statusbar[kThemeableBrowserPropStatusBarStyle]){ + NSString* style = browserOptions.statusbar[kThemeableBrowserPropStatusBarStyle]; + if([style isEqualToString:@"lightcontent"]){ + statusBarStyle = UIStatusBarStyleLightContent; + }else if([style isEqualToString:@"darkcontent"]){ + if (@available(iOS 13.0, *)) { + statusBarStyle = UIStatusBarStyleDarkContent; } } } - + if (self.themeableBrowserViewController == nil) { - NSString* originalUA = [CDVUserAgentUtil originalUserAgent]; self.themeableBrowserViewController = [[CDVThemeableBrowserViewController alloc] - initWithUserAgent:originalUA prevUserAgent:[self.commandDelegate userAgent] - browserOptions: browserOptions + init: browserOptions navigationDelete:self - statusBarStyle:[UIApplication sharedApplication].statusBarStyle]; - + statusBarStyle:statusBarStyle]; + self.themeableBrowserViewController.navigationDelegate = self; + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { self.themeableBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; } } - + [self.themeableBrowserViewController showLocationBar:browserOptions.location]; [self.themeableBrowserViewController showToolBar:YES:browserOptions.toolbarposition]; if (browserOptions.closebuttoncaption != nil) { @@ -264,7 +312,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } self.themeableBrowserViewController.modalPresentationStyle = presentationStyle; - + // Set Transition Style UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default if (browserOptions.transitionstyle != nil) { @@ -275,7 +323,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } self.themeableBrowserViewController.modalTransitionStyle = transitionStyle; - + // prevent webView from bouncing if (browserOptions.disallowoverscroll) { if ([self.themeableBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { @@ -288,16 +336,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } } - - // UIWebView options - self.themeableBrowserViewController.webView.scalesPageToFit = browserOptions.zoom; - self.themeableBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; - self.themeableBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; - if (IsAtLeastiOSVersion(@"6.0")) { - self.themeableBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; - self.themeableBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; - } - + [self.themeableBrowserViewController navigateTo:url]; if (!browserOptions.hidden) { [self show:nil withAnimation:!browserOptions.disableAnimation]; @@ -321,17 +360,34 @@ - (void)show:(CDVInvokedUrlCommand*)command withAnimation:(BOOL)animated withMessage:@"Show called but already shown"]; return; } - + _isShown = YES; - + CDVThemeableBrowserNavigationController* nav = [[CDVThemeableBrowserNavigationController alloc] - initWithRootViewController:self.themeableBrowserViewController]; + initWithRootViewController:self.themeableBrowserViewController]; nav.orientationDelegate = self.themeableBrowserViewController; nav.navigationBarHidden = YES; + if (@available(iOS 13.0, *)) { + nav.modalPresentationStyle = UIModalPresentationOverFullScreen; + } + + __weak CDVThemeableBrowser* weakSelf = self; + // Run later to avoid the "took a long time" log message. dispatch_async(dispatch_get_main_queue(), ^{ if (self.themeableBrowserViewController != nil) { - [self.viewController presentViewController:nav animated:animated completion:nil]; + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf->tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + strongSelf->tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } + + UIViewController *tmpController = [[UIViewController alloc] init]; + [strongSelf->tmpWindow setRootViewController:tmpController]; + + + [strongSelf->tmpWindow makeKeyAndVisible]; + [tmpController presentViewController:nav animated:YES completion:nil]; } }); } @@ -339,7 +395,7 @@ - (void)show:(CDVInvokedUrlCommand*)command withAnimation:(BOOL)animated - (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options { NSURLRequest* request = [NSURLRequest requestWithURL:url]; - + #ifdef __CORDOVA_4_0_0 // the webview engine itself will filter for this according to policy // in config.xml for cordova-ios-4.0 @@ -373,31 +429,43 @@ - (void)openInSystem:(NSURL*)url - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper { - if (!_injectedIframeBridge) { - _injectedIframeBridge = YES; - // Create an iframe bridge in the new document to communicate with the CDVThemeableBrowserViewController - [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);})(document)"]; - } - + // Ensure a message handler bridge is created to communicate with the CDVWKthemeableBrowserViewController + [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.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; + [self evaluateJavaScript:jsToInject]; } } else { - [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; + [self evaluateJavaScript:source]; } } + +//Synchronus helper for javascript evaluation +- (void)evaluateJavaScript:(NSString *)script { + __block NSString* _script = script; + [self.themeableBrowserViewController.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:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + jsWrapper = [NSString stringWithFormat:@"_cdvMessageHandler('%@',JSON.stringify([eval(%%@)]));", command.callbackId]; } [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; } @@ -405,9 +473,9 @@ - (void)injectScriptCode:(CDVInvokedUrlCommand*)command - (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() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + 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)"; } @@ -417,9 +485,9 @@ - (void)injectScriptFile:(CDVInvokedUrlCommand*)command - (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() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + 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)"; } @@ -429,9 +497,9 @@ - (void)injectStyleCode:(CDVInvokedUrlCommand*)command - (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() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + 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)"; } @@ -456,131 +524,139 @@ - (BOOL)isValidCallbackId:(NSString *)callbackId } /** - * The iframe bridge provided for the ThemeableBrowser is capable of executing any oustanding callback belonging - * to the ThemeableBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that 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. - * - * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: - * - * gap-iab:/// - * - * where is the string id of the callback to trigger (something like "ThemeableBrowser0123456789") - * - * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded - * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION - * is returned if the JSON is invalid. */ -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType -{ - NSURL* url = request.URL; - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - - // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, - // and the path, if present, should be a JSON-encoded value to pass to the callback. - if ([[url scheme] isEqualToString:@"gap-iab"]) { - NSString* scriptCallbackId = [url host]; - CDVPluginResult* pluginResult = nil; - - if ([self isValidCallbackId:scriptCallbackId]) { - NSString* scriptResult = [url path]; - NSError* __autoreleasing error = nil; - - // The message should be a JSON-encoded array of the result of the script which executed. - if ((scriptResult != nil) && ([scriptResult length] > 1)) { - scriptResult = [scriptResult substringFromIndex:1]; - 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]; - return NO; - } - } else if ([self isSystemUrl:url]) { - // Do not allow iTunes store links from ThemeableBrowser as they do not work - // instead open them with App Store app or Safari - [[UIApplication sharedApplication] openURL:url]; - - // only in the case where a redirect link is opened in a freshly started - // ThemeableBrowser frame, trigger ThemeableBrowserRedirectExternalOnOpen - // event. This event can be handled in the app-side -- for instance, to - // close the ThemeableBrowser as the frame will contain a blank page - if ( - originalUrl != nil - && [[originalUrl absoluteString] isEqualToString:[initUrl absoluteString]] - && _framesOpened == 1 - ) { - NSDictionary *event = @{ - @"type": @"ThemeableBrowserRedirectExternalOnOpen", - @"message": @"ThemeableBrowser redirected to open an external app on fresh start" - }; - - [self emitEvent:event]; - } - - // do not load content in the web view since this URL is handled by an - // external app - return NO; - } else if ((self.callbackId != nil) && isTopLevelNavigation) { +- (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; + + //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]; } - - // originalUrl is used to detect redirect. This works by storing the - // request URL of the original frame when it's about to be loaded. A redirect - // will cause shouldStartLoadWithRequest to be called again before the - // original frame finishes loading (originalUrl becomes nil upon the frame - // finishing loading). On second time shouldStartLoadWithRequest - // is called, this stored original frame's URL can be compared against - // the URL of the new request. A mismatch implies redirect. - originalUrl = request.URL; - - return 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)webViewDidStartLoad:(UIWebView*)theWebView +- (void)didStartProvisionalNavigation:(WKWebView*)theWebView { - _injectedIframeBridge = NO; - _framesOpened++; + NSLog(@"didStartProvisionalNavigation"); +// self.inAppBrowserViewController.currentURL = theWebView.URL; } -- (void)webViewDidFinishLoad:(UIWebView*)theWebView +- (void)didFinishNavigation:(WKWebView*)theWebView { if (self.callbackId != nil) { - // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). - NSString* url = [self.themeableBrowserViewController.currentURL absoluteString]; + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.themeableBrowserViewController.currentURL != nil){ + url = [self.themeableBrowserViewController.currentURL absoluteString]; + }else{ + url = @""; + } + } CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - - // once a web view finished loading a frame, reset the stored original - // URL of the frame so that it can be used to detect next redirection - originalUrl = nil; - + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } } -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +- (void)webView:(WKWebView*)theWebView didFailNavigation:(NSError*)error { if (self.callbackId != nil) { - NSString* url = [self.themeableBrowserViewController.currentURL absoluteString]; + NSString* url = [theWebView.URL absoluteString]; + if(url == nil){ + if(self.themeableBrowserViewController.currentURL != nil){ + url = [self.themeableBrowserViewController.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]; } } +- (UIWindow*)getTmpWindow +{ + // Set tmpWindow to hidden to make main webview responsive to touch again + // Based on https://stackoverflow.com/questions/4544489/how-to-remove-a-uiwindow + return self->tmpWindow; +} + +- (void) nilTmpWindow{ + self->tmpWindow = nil; +} + - (void)browserExit { if (self.callbackId != nil) { @@ -589,14 +665,26 @@ - (void)browserExit [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; self.callbackId = nil; } + + [self.themeableBrowserViewController.configuration.userContentController removeScriptMessageHandlerForName:IAB_BRIDGE_NAME]; + self.themeableBrowserViewController.configuration = nil; + + [self.themeableBrowserViewController.webView stopLoading]; + [self.themeableBrowserViewController.webView removeFromSuperview]; + [self.themeableBrowserViewController.webView setUIDelegate:nil]; + [self.themeableBrowserViewController.webView setNavigationDelegate:nil]; + self.themeableBrowserViewController.webView = nil; + // Set navigationDelegate to nil to ensure no callbacks are received from it. self.themeableBrowserViewController.navigationDelegate = nil; // Don't recycle the ViewController since it may be consuming a lot of memory. // Also - this is required for the PDF/User-Agent bug work-around. self.themeableBrowserViewController = nil; + + self.callbackId = nil; self.callbackIdPattern = nil; - + _framesOpened = 0; _isShown = NO; } @@ -607,7 +695,7 @@ - (void)emitEvent:(NSDictionary*)event CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:event]; [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; - + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } } @@ -615,22 +703,22 @@ - (void)emitEvent:(NSDictionary*)event - (void)emitError:(NSString*)code withMessage:(NSString*)message { NSDictionary *event = @{ - @"type": kThemeableBrowserEmitError, - @"code": code, - @"message": message - }; - + @"type": kThemeableBrowserEmitError, + @"code": code, + @"message": message + }; + [self emitEvent:event]; } - (void)emitWarning:(NSString*)code withMessage:(NSString*)message { NSDictionary *event = @{ - @"type": kThemeableBrowserEmitWarning, - @"code": code, - @"message": message - }; - + @"type": kThemeableBrowserEmitWarning, + @"code": code, + @"message": message + }; + [self emitEvent:event]; } @@ -642,55 +730,81 @@ @implementation CDVThemeableBrowserViewController @synthesize currentURL; -- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle +- (id)init:(CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle { self = [super init]; if (self != nil) { - _userAgent = userAgent; - _prevUserAgent = prevUserAgent; + _lastReducedStatusBarHeight = 0.0; _browserOptions = browserOptions; -#ifdef __CORDOVA_4_0_0 - _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; -#else - _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; -#endif + self.webViewUIDelegate = [[CDVThemeableBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; + [self.webViewUIDelegate setViewController:self]; _navigationDelegate = navigationDelegate; _statusBarStyle = statusBarStyle; + _initialStatusBarHeight = [self getStatusBarHeight]; [self createViews]; } - + return self; } - (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:kThemeableBrowserToolbarBarPositionTop]; NSDictionary* toolbarProps = _browserOptions.toolbar; - CGFloat toolbarHeight = [self getFloatFromDict:toolbarProps withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - if (!_browserOptions.fullscreen) { - webViewBounds.size.height -= toolbarHeight; + CGFloat toolbarOffsetHeight = [self getOffsetToolbarHeight]; + + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + configuration.userContentController = userContentController; +#if __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")) { + if(_browserOptions.mediaplaybackrequiresuseraction == YES){ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + }else{ + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } + }else{ // iOS 9 + configuration.mediaPlaybackRequiresUserAction = _browserOptions.mediaplaybackrequiresuseraction; } - self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; - + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - + [self.view addSubview:self.webView]; [self.view sendSubviewToBack:self.webView]; - - self.webView.delegate = _webViewDelegate; + + self.webView.navigationDelegate = self; + self.webView.UIDelegate = self.webViewUIDelegate; self.webView.backgroundColor = [UIColor whiteColor]; - + self.webView.clearsContextBeforeDrawing = YES; self.webView.clipsToBounds = YES; self.webView.contentMode = UIViewContentModeScaleToFill; self.webView.multipleTouchEnabled = YES; self.webView.opaque = YES; - self.webView.scalesPageToFit = NO; 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:UIActivityIndicatorViewStyleWhite]; self.spinner.alpha = 1.000; self.spinner.autoresizesSubviews = YES; @@ -705,10 +819,10 @@ - (void)createViews self.spinner.opaque = NO; self.spinner.userInteractionEnabled = NO; [self.spinner stopAnimating]; - - CGFloat toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - toolbarHeight : 0.0; - CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, toolbarHeight); - + + CGFloat toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - toolbarOffsetHeight : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, toolbarOffsetHeight); + self.toolbar = [[UIView alloc] initWithFrame:toolbarFrame]; self.toolbar.alpha = 1.000; self.toolbar.autoresizesSubviews = YES; @@ -721,12 +835,13 @@ - (void)createViews self.toolbar.opaque = NO; self.toolbar.userInteractionEnabled = YES; self.toolbar.backgroundColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:toolbarProps withKey:kThemeableBrowserPropColor withDefault:@"#ffffffff"]]; - + if (toolbarProps[kThemeableBrowserPropImage] || toolbarProps[kThemeableBrowserPropWwwImage]) { UIImage *image = [self getImage:toolbarProps[kThemeableBrowserPropImage] - altPath:toolbarProps[kThemeableBrowserPropWwwImage] - altDensity:[toolbarProps[kThemeableBrowserPropWwwImageDensity] doubleValue]]; - + altPath:toolbarProps[kThemeableBrowserPropWwwImage] + altDensity:[toolbarProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription:@""]; + if (image) { self.toolbar.backgroundColor = [UIColor colorWithPatternImage:image]; } else { @@ -736,10 +851,10 @@ - (void)createViews ? toolbarProps[kThemeableBrowserPropImage] : toolbarProps[kThemeableBrowserPropWwwImage]]]; } } - + CGFloat labelInset = 5.0; float locationBarY = 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; @@ -753,13 +868,13 @@ - (void)createViews 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; @@ -768,23 +883,23 @@ - (void)createViews self.addressLabel.textAlignment = NSTextAlignmentLeft; self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; self.addressLabel.userInteractionEnabled = NO; - + self.closeButton = [self createButton:_browserOptions.closeButton action:@selector(close) withDescription:@"close button"]; self.backButton = [self createButton:_browserOptions.backButton action:@selector(goBack:) withDescription:@"back button"]; self.forwardButton = [self createButton:_browserOptions.forwardButton action:@selector(goForward:) withDescription:@"forward button"]; self.menuButton = [self createButton:_browserOptions.menu action:@selector(goMenu:) withDescription:@"menu button"]; - + // Arramge toolbar buttons with respect to user configuration. CGFloat leftWidth = 0; CGFloat rightWidth = 0; - + // Both left and right side buttons will be ordered from outside to inside. NSMutableArray* leftButtons = [NSMutableArray new]; NSMutableArray* rightButtons = [NSMutableArray new]; - + if (self.closeButton) { CGFloat width = [self getWidthFromButton:self.closeButton]; - + if ([kThemeableBrowserAlignRight isEqualToString:_browserOptions.closeButton[kThemeableBrowserPropAlign]]) { [rightButtons addObject:self.closeButton]; rightWidth += width; @@ -793,10 +908,10 @@ - (void)createViews leftWidth += width; } } - + if (self.menuButton) { CGFloat width = [self getWidthFromButton:self.menuButton]; - + if ([kThemeableBrowserAlignRight isEqualToString:_browserOptions.menu[kThemeableBrowserPropAlign]]) { [rightButtons addObject:self.menuButton]; rightWidth += width; @@ -805,7 +920,7 @@ - (void)createViews leftWidth += width; } } - + // Back and forward buttons must be added with special ordering logic such // that back button is always on the left of forward button if both buttons // are on the same side. @@ -814,25 +929,25 @@ - (void)createViews [leftButtons addObject:self.backButton]; leftWidth += width; } - + if (self.forwardButton && [kThemeableBrowserAlignRight isEqualToString:_browserOptions.forwardButton[kThemeableBrowserPropAlign]]) { CGFloat width = [self getWidthFromButton:self.forwardButton]; [rightButtons addObject:self.forwardButton]; rightWidth += width; } - + if (self.forwardButton && ![kThemeableBrowserAlignRight isEqualToString:_browserOptions.forwardButton[kThemeableBrowserPropAlign]]) { CGFloat width = [self getWidthFromButton:self.forwardButton]; [leftButtons addObject:self.forwardButton]; leftWidth += width; } - + if (self.backButton && [kThemeableBrowserAlignRight isEqualToString:_browserOptions.backButton[kThemeableBrowserPropAlign]]) { CGFloat width = [self getWidthFromButton:self.backButton]; [rightButtons addObject:self.backButton]; rightWidth += width; } - + NSArray* customButtons = _browserOptions.customButtons; if (customButtons) { NSInteger cnt = 0; @@ -850,42 +965,54 @@ - (void)createViews leftWidth += width; } } - + cnt += 1; } } - + self.rightButtons = rightButtons; self.leftButtons = leftButtons; - + for (UIButton* button in self.leftButtons) { [self.toolbar addSubview:button]; } - + for (UIButton* button in self.rightButtons) { [self.toolbar addSubview:button]; } - + [self layoutButtons]; - - self.titleOffset = fmaxf(leftWidth, rightWidth); + + self.titleOffsetLeft = leftWidth; + self.titleOffsetRight = rightWidth; + self.toolbarPaddingX = 0; + if (_browserOptions.toolbar[kThemeableBrowserPropToolbarPaddingX]) { + self.toolbarPaddingX = [_browserOptions.toolbar[kThemeableBrowserPropToolbarPaddingX] floatValue]; + } + + // The correct positioning of title is not that important right now, since // rePositionViews will take care of it a bit later. self.titleLabel = nil; if (_browserOptions.title) { - self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, 10, toolbarHeight)]; + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, 10, toolbarOffsetHeight)]; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.titleLabel.numberOfLines = 1; self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.titleLabel.textColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:_browserOptions.title withKey:kThemeableBrowserPropColor withDefault:@"#000000ff"]]; - + if (_browserOptions.title[kThemeableBrowserPropStaticText]) { self.titleLabel.text = _browserOptions.title[kThemeableBrowserPropStaticText]; } - + + if (_browserOptions.title[kThemeableBrowserPropTitleFontSize]) { + CGFloat fontSize = [_browserOptions.title[kThemeableBrowserPropTitleFontSize] floatValue]; + self.titleLabel.font = [self.titleLabel.font fontWithSize:fontSize]; + } + [self.toolbar addSubview:self.titleLabel]; } - + self.view.backgroundColor = [CDVThemeableBrowserViewController colorFromRGBA:[self getStringFromDict:_browserOptions.statusbar withKey:kThemeableBrowserPropColor withDefault:@"#ffffffff"]]; [self.view addSubview:self.toolbar]; // [self.view addSubview:self.addressLabel]; @@ -903,7 +1030,7 @@ - (void)createViews * bundle, we can't tell what densitiy the image is supposed to be so it needs to be given * explicitly. */ -- (UIImage*) getImage:(NSString*) name altPath:(NSString*) altPath altDensity:(CGFloat) altDensity +- (UIImage*) getImage:(NSString*) name altPath:(NSString*) altPath altDensity:(CGFloat) altDensity accessibilityDescription:(NSString*) accessibilityDescription { UIImage* result = nil; if (name) { @@ -916,8 +1043,10 @@ - (UIImage*) getImage:(NSString*) name altPath:(NSString*) altPath altDensity:(C } NSData* data = [NSData dataWithContentsOfFile:path]; result = [UIImage imageWithData:data scale:altDensity]; + result.accessibilityLabel = accessibilityDescription; + result.isAccessibilityElement = true; } - + return result; } @@ -926,11 +1055,17 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe UIButton* result = nil; if (buttonProps) { UIImage *buttonImage = nil; + NSString* accessibilityDescription = description; + if(buttonProps[kThemeableBrowserPropAccessibilityDescription]){ + accessibilityDescription = buttonProps[kThemeableBrowserPropAccessibilityDescription]; + } if (buttonProps[kThemeableBrowserPropImage] || buttonProps[kThemeableBrowserPropWwwImage]) { buttonImage = [self getImage:buttonProps[kThemeableBrowserPropImage] - altPath:buttonProps[kThemeableBrowserPropWwwImage] - altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]]; - + altPath:buttonProps[kThemeableBrowserPropWwwImage] + altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription: accessibilityDescription + ]; + if (!buttonImage) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail withMessage:[NSString stringWithFormat:@"Image for %@, %@, failed to load.", @@ -940,15 +1075,17 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe } } else { [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined - withMessage:[NSString stringWithFormat:@"Image for %@ is not defined. Button will not be shown.", description]]; + withMessage:[NSString stringWithFormat:@"Image for %@ is not defined. Button will not be shown.", description]]; } - + UIImage *buttonImagePressed = nil; if (buttonProps[kThemeableBrowserPropImagePressed] || buttonProps[kThemeableBrowserPropWwwImagePressed]) { buttonImagePressed = [self getImage:buttonProps[kThemeableBrowserPropImagePressed] - altPath:buttonProps[kThemeableBrowserPropWwwImagePressed] - altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]];; - + altPath:buttonProps[kThemeableBrowserPropWwwImagePressed] + altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription: accessibilityDescription + ];; + if (!buttonImagePressed) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail withMessage:[NSString stringWithFormat:@"Pressed image for %@, %@, failed to load.", @@ -958,18 +1095,18 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe } } else { [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined - withMessage:[NSString stringWithFormat:@"Pressed image for %@ is not defined.", description]]; + withMessage:[NSString stringWithFormat:@"Pressed image for %@ is not defined.", description]]; } - + if (buttonImage) { result = [UIButton buttonWithType:UIButtonTypeCustom]; result.bounds = CGRectMake(0, 0, buttonImage.size.width, buttonImage.size.height); - + if (buttonImagePressed) { [result setImage:buttonImagePressed forState:UIControlStateHighlighted]; result.adjustsImageWhenHighlighted = NO; } - + [result setImage:buttonImage forState:UIControlStateNormal]; [result addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; } @@ -978,14 +1115,14 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe withMessage:[NSString stringWithFormat:@"%@ is not defined. Button will not be shown.", description]]; } else if (!buttonProps[kThemeableBrowserPropImage]) { } - + return result; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { [super didRotateFromInterfaceOrientation:fromInterfaceOrientation]; - + // Reposition views. [self rePositionViews]; } @@ -998,19 +1135,22 @@ - (void)layoutButtons { CGFloat screenWidth = CGRectGetWidth(self.view.frame); CGFloat toolbarHeight = self.toolbar.frame.size.height; - + CGFloat toolbarPadding = _browserOptions.fullscreen ? [self getStatusBarHeight] : 0.0; + // Layout leftButtons and rightButtons from outer to inner. - CGFloat left = 0; + CGFloat left = self.toolbarPaddingX; for (UIButton* button in self.leftButtons) { CGSize size = button.frame.size; - button.frame = CGRectMake(left, floorf((toolbarHeight - size.height) / 2), size.width, size.height); + CGFloat yOffset = floorf((toolbarHeight + (toolbarPadding/2) - size.height) / 2); + button.frame = CGRectMake(left, yOffset, size.width, size.height); left += size.width; } - - CGFloat right = 0; + + CGFloat right = self.toolbarPaddingX; for (UIButton* button in self.rightButtons) { CGSize size = button.frame.size; - button.frame = CGRectMake(screenWidth - right - size.width, floorf((toolbarHeight - size.height) / 2), size.width, size.height); + CGFloat yOffset = floorf((toolbarHeight + (toolbarPadding/2) - size.height) / 2); + button.frame = CGRectMake(screenWidth - right - size.width, yOffset, size.width, size.height); right += size.width; } } @@ -1019,14 +1159,14 @@ - (void)setCloseButtonTitle:(NSString*)title { // This method is not used by ThemeableBrowser. It is inherited from // InAppBrowser and is kept for merge purposes. - + // 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; // self.closeButton = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)]; // self.closeButton.enabled = YES; // self.closeButton.tintColor = [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:0 withObject:self.closeButton]; // [self.toolbar setItems:items]; @@ -1035,51 +1175,45 @@ - (void)setCloseButtonTitle:(NSString*)title - (void)showLocationBar:(BOOL)show { CGRect locationbarFrame = self.addressLabel.frame; - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - + CGFloat toolbarHeight = [self getOffsetToolbarHeight]; + 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; - if (!_browserOptions.fullscreen) { - webViewBounds.size.height -= toolbarHeight; - } [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; - if (!_browserOptions.fullscreen) { - webViewBounds.size.height -= toolbarHeight; - } [self setWebViewFrame:webViewBounds]; } else { // no toolBar, expand webView to screen dimensions @@ -1092,25 +1226,22 @@ - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition { CGRect toolbarFrame = self.toolbar.frame; CGRect locationbarFrame = self.addressLabel.frame; - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - + CGFloat toolbarHeight = [self getOffsetToolbarHeight]; + 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 - if (!_browserOptions.fullscreen) { - webViewBounds.size.height -= toolbarHeight; - } locationbarFrame.origin.y = webViewBounds.size.height; self.addressLabel.frame = locationbarFrame; self.toolbar.frame = toolbarFrame; @@ -1118,7 +1249,7 @@ - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition // no locationBar, so put toolBar at the bottom self.toolbar.frame = toolbarFrame; } - + if ([toolbarPosition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { toolbarFrame.origin.y = 0; if (!_browserOptions.fullscreen) { @@ -1129,19 +1260,19 @@ - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition 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; @@ -1160,35 +1291,60 @@ - (void)viewDidLoad - (void)viewDidUnload { [self.webView loadHTMLString:nil baseURL:nil]; - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.webView.UIDelegate = nil; [super viewDidUnload]; } +- (void) viewDidDisappear:(BOOL)animated +{ + _lastReducedStatusBarHeight = 0; +} + - (UIStatusBarStyle)preferredStatusBarStyle { - return _statusBarStyle; + UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; + if(_browserOptions.statusbar[kThemeableBrowserPropStatusBarStyle]){ + NSString* style = _browserOptions.statusbar[kThemeableBrowserPropStatusBarStyle]; + if([style isEqualToString:@"lightcontent"]){ + statusBarStyle = UIStatusBarStyleLightContent; + }else if([style isEqualToString:@"darkcontent"]){ + if (@available(iOS 13.0, *)) { + statusBarStyle = UIStatusBarStyleDarkContent; + } + } + } + return statusBarStyle; +} + +- (BOOL) prefersStatusBarHidden{ + return _browserOptions.fullscreen; } - (void)close { [self emitEventForButton:_browserOptions.closeButton]; - - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; - - if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { - [self.navigationDelegate browserExit]; - } - + self.webView.UIDelegate = nil; + CDVThemeableBrowser* navigationDelegate = self.navigationDelegate; + // Run later to avoid the "took a long time" log message. dispatch_async(dispatch_get_main_queue(), ^{ if ([self respondsToSelector:@selector(presentingViewController)]) { - [[self presentingViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil]; + [[self presentingViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:^{ + [navigationDelegate nilTmpWindow]; + }]; } else { - [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil]; + [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:^{ + [navigationDelegate nilTmpWindow]; + }]; } }); - + + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } + } - (void)reload @@ -1199,25 +1355,17 @@ - (void)reload - (void)navigateTo:(NSURL*)url { NSURLRequest* request = [NSURLRequest requestWithURL:url]; - - if (_userAgentLockToken != 0) { - [self.webView loadRequest:request]; - } else { - [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { - _userAgentLockToken = lockToken; - [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; - [self.webView loadRequest:request]; - }]; - } + + [self.webView loadRequest:request]; } - (void)goBack:(id)sender { [self emitEventForButton:_browserOptions.backButton]; - + if (self.webView.canGoBack) { [self.webView goBack]; - [self updateButtonDelayed:self.webView]; + [self updateButton:self.webView]; } else if (_browserOptions.backButtonCanClose) { [self close]; } @@ -1226,9 +1374,9 @@ - (void)goBack:(id)sender - (void)goForward:(id)sender { [self emitEventForButton:_browserOptions.forwardButton]; - + [self.webView goForward]; - [self updateButtonDelayed:self.webView]; + [self updateButton:self.webView]; } - (void)goCustomButton:(id)sender @@ -1241,7 +1389,7 @@ - (void)goCustomButton:(id)sender - (void)goMenu:(id)sender { [self emitEventForButton:_browserOptions.menu]; - + if (_browserOptions.menu && _browserOptions.menu[kThemeableBrowserPropItems]) { NSArray* menuItems = _browserOptions.menu[kThemeableBrowserPropItems]; if (IsAtLeastiOSVersion(@"8.0")) { @@ -1252,23 +1400,23 @@ - (void)goMenu:(id)sender message:nil preferredStyle:UIAlertControllerStyleActionSheet]; alertController.popoverPresentationController.sourceView - = self.menuButton; + = self.menuButton; alertController.popoverPresentationController.sourceRect - = self.menuButton.bounds; - + = self.menuButton.bounds; + for (NSInteger i = 0; i < menuItems.count; i++) { NSInteger index = i; NSDictionary *item = menuItems[index]; - + UIAlertAction *a = [UIAlertAction - actionWithTitle:item[@"label"] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self menuSelected:index]; - }]; + actionWithTitle:item[@"label"] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self menuSelected:index]; + }]; [alertController addAction:a]; } - + if (_browserOptions.menu[kThemeableBrowserPropCancel]) { UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:_browserOptions.menu[kThemeableBrowserPropCancel] @@ -1276,14 +1424,14 @@ - (void)goMenu:(id)sender handler:nil]; [alertController addAction:cancelAction]; } - + [self presentViewController:alertController animated:YES completion:nil]; } else { // iOS < 8 implementation using UIActionSheet, which is deprecated. UIActionSheet *popup = [[UIActionSheet alloc] initWithTitle:_browserOptions.menu[kThemeableBrowserPropTitle] delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil]; - + for (NSDictionary *item in menuItems) { [popup addButtonWithTitle:item[@"label"]]; } @@ -1291,7 +1439,7 @@ - (void)goMenu:(id)sender [popup addButtonWithTitle:_browserOptions.menu[kThemeableBrowserPropCancel]]; popup.cancelButtonIndex = menuItems.count; } - + [popup showFromRect:self.menuButton.frame inView:self.view animated:YES]; } } else { @@ -1319,39 +1467,136 @@ - (void)viewWillAppear:(BOOL)animated [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; } [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 { - CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; - float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; - return statusBarOffset; + +- (CGFloat) getStatusBarHeight { + return [[UIApplication sharedApplication] statusBarFrame].size.height; } -- (void) rePositionViews { - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight; +- (CGFloat) getStatusBarOffset { + CGFloat offset = 0; + if(_browserOptions.fullscreen){ + if(![self isPortrait]){ + offset = [self getTopSafeAreaInset]; + } + }else{ + offset = [self getStatusBarHeight]; + } + return offset; +} + +- (BOOL) isPortrait{ + return [[UIDevice currentDevice] orientation] == UIDeviceOrientationPortrait; +} + +- (BOOL)hasTopNotch { + return [self getTopSafeAreaInset] > 20.0; +} - if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, self.webView.frame.size.height)]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; +- (CGFloat) getTopSafeAreaInset { + if (@available(iOS 13.0, *)) { + return [self keyWindow].safeAreaInsets.top; + }else if (@available(iOS 11.0, *)){ + return [[[UIApplication sharedApplication] delegate] window].safeAreaInsets.top; } + return 0.0; +} - CGFloat screenWidth = CGRectGetWidth(self.view.frame); - NSInteger width = floorf(screenWidth - self.titleOffset * 2.0f); - if (self.titleLabel) { - self.titleLabel.frame = CGRectMake(floorf((screenWidth - width) / 2.0f), 0, width, toolbarHeight); +- (UIWindow*)keyWindow { + UIWindow *foundWindow = nil; + NSArray *windows = [[UIApplication sharedApplication]windows]; + for (UIWindow *window in windows) { + if (window.isKeyWindow) { + foundWindow = window; + break; + } } + return foundWindow; +} + +-(CGFloat) getToolbarTopSafeAreaOffset { + return _browserOptions.fullscreen ? [self getTopSafeAreaInset] : 0.0; +} + +-(CGFloat) getOffsetToolbarHeight { + return [self getToolbarHeight] + [self getToolbarTopSafeAreaOffset]; +} +-(CGFloat) getToolbarHeight { + return [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; +} + +- (void) rePositionViews { + + CGRect viewBounds = [self.webView bounds]; + CGFloat statusBarOffset = [self getStatusBarOffset]; + CGFloat toolbarHeight = [self getToolbarHeight]; + CGFloat toolbarTopSafeAreaOffset = [self getToolbarTopSafeAreaOffset]; + + // 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 = statusBarOffset; + + // account for web view height portion that may have been reduced by a previous call to this method + viewBounds.size.height = viewBounds.size.height - statusBarOffset + (_browserOptions.fullscreen ? 0 : _lastReducedStatusBarHeight); + _lastReducedStatusBarHeight = statusBarOffset; + + CGFloat initialWebViewHeight = self.view.frame.size.height; + + if ((_browserOptions.toolbar) && ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop])) { + // if we have to display the toolbar on top of the web view, we need to account for its height + CGFloat webViewOffset = [self getToolbarHeight] + (_browserOptions.fullscreen || [self isPortrait] ? _initialStatusBarHeight : 0) + (_browserOptions.fullscreen ? _lastReducedStatusBarHeight : 0); + viewBounds.origin.y = webViewOffset; + + CGFloat webViewHeight = initialWebViewHeight - webViewOffset; + viewBounds.size.height = webViewHeight; + + self.toolbar.frame = CGRectMake(self.toolbar.frame.origin.x, statusBarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height); + } + self.webView.frame = viewBounds; + + + + + if (self.titleLabel) { + CGFloat screenWidth = CGRectGetWidth(self.view.frame); + NSInteger width = floorf(screenWidth - (self.titleOffsetLeft + self.titleOffsetRight)); + CGFloat leftOffset; + if(self.titleOffsetLeft > 0 && self.titleOffsetRight > 0){ + leftOffset = floorf((screenWidth - width) / 2.0f); + }else if(self.titleOffsetLeft > 0){ + leftOffset = self.titleOffsetLeft; + }else{ + leftOffset = self.toolbarPaddingX; + } + + CGFloat toolbarHeight = self.toolbar.frame.size.height; + CGFloat toolbarPadding = _browserOptions.fullscreen ? [self getStatusBarHeight] : 0.0; + CGSize size = self.titleLabel.frame.size; + CGFloat yOffset = floorf((toolbarHeight + (toolbarPadding/2) - size.height) / 2); + + self.titleLabel.frame = CGRectMake(leftOffset, yOffset, width, toolbarHeight); + } + [self layoutButtons]; } +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [coordinator animateAlongsideTransition:^(id context) + { + [self rePositionViews]; + } completion:^(id context) + { + + }]; + + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; +} + - (CGFloat) getFloatFromDict:(NSDictionary*)dict withKey:(NSString*)key withDefault:(CGFloat)def { CGFloat result = def; @@ -1391,124 +1636,128 @@ - (void)emitEventForButton:(NSDictionary*)buttonProps - (void)emitEventForButton:(NSDictionary*)buttonProps withIndex:(NSNumber*)index { - if (buttonProps) { - NSString* event = buttonProps[kThemeableBrowserPropEvent]; - if (event) { - NSMutableDictionary* dict = [NSMutableDictionary new]; - [dict setObject:event forKey:@"type"]; - [dict setObject:[self.navigationDelegate.themeableBrowserViewController.currentURL absoluteString] forKey:@"url"]; - - if (index) { - [dict setObject:index forKey:@"index"]; + @try { + if (buttonProps) { + NSString* event = buttonProps[kThemeableBrowserPropEvent]; + if (event) { + NSMutableDictionary* dict = [NSMutableDictionary new]; + [dict setObject:event forKey:@"type"]; + NSString* url = [self.navigationDelegate.themeableBrowserViewController.currentURL absoluteString]; + if(url != nil){ + [dict setObject:url forKey:@"url"]; + } + + if (index) { + [dict setObject:index forKey:@"index"]; + } + [self.navigationDelegate emitEvent:dict]; + } else { + [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined + withMessage:@"Button clicked, but event property undefined. No event will be raised."]; } - [self.navigationDelegate emitEvent:dict]; - } else { - [self.navigationDelegate emitWarning:kThemeableBrowserEmitCodeUndefined - withMessage:@"Button clicked, but event property undefined. No event will be raised."]; } + }@catch (NSException *exception) { + NSLog(@"EXCEPTION on emitEventForButton: %@", exception.reason); } } -#pragma mark UIWebViewDelegate - -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ - // loading url, start spinner +#pragma mark WKNavigationDelegate +- (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigation *)navigation{ + + // loading url, start spinner, update back/forward + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - - [self.spinner startAnimating]; - - return [self.navigationDelegate webViewDidStartLoad:theWebView]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner startAnimating]; + + return [self.navigationDelegate didStartProvisionalNavigation:theWebView]; } -- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - + NSURL *url = navigationAction.request.URL; + NSURL *mainDocumentURL = navigationAction.request.mainDocumentURL; + + BOOL isTopLevelNavigation = [url isEqual:mainDocumentURL]; + if (isTopLevelNavigation) { - self.currentURL = request.URL; + self.currentURL = url; } - - [self updateButtonDelayed:theWebView]; - - return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; + + [self.navigationDelegate webView:theWebView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler]; } -- (void)webViewDidFinishLoad:(UIWebView*)theWebView +- (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navigation { // update url, stop spinner, update back/forward - + self.addressLabel.text = [self.currentURL absoluteString]; - [self updateButton:theWebView]; - + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + theWebView.scrollView.contentInset = UIEdgeInsetsZero; + if (self.titleLabel && _browserOptions.title - && !_browserOptions.title[kThemeableBrowserPropStaticText] - && [self getBoolFromDict:_browserOptions.title withKey:kThemeableBrowserPropShowPageTitle]) { + && !_browserOptions.title[kThemeableBrowserPropStaticText] + && [self getBoolFromDict:_browserOptions.title withKey:kThemeableBrowserPropShowPageTitle]) { // Update title text to page title when title is shown and we are not // required to show a static text. - self.titleLabel.text = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + [self.webView evaluateJavaScript:@"document.title" completionHandler:^(NSString* title, NSError* _Nullable error) { + self.titleLabel.text = title; + }]; } - + [self.spinner stopAnimating]; - - // Work around a bug where the first time a PDF is opened, all UIWebViews - // reload their User-Agent from NSUserDefaults. - // This work-around makes the following assumptions: - // 1. The app has only a single Cordova Webview. If not, then the app should - // take it upon themselves to load a PDF in the background as a part of - // their start-up flow. - // 2. That the PDF does not require any additional network requests. We change - // the user-agent here back to that of the CDVViewController, so requests - // from it must pass through its white-list. This *does* break PDFs that - // contain links to other remote PDF/websites. - // More info at https://issues.apache.org/jira/browse/CB-2225 - BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; - if (isPDF) { - [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; - } - - [self.navigationDelegate webViewDidFinishLoad:theWebView]; + + + [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:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +- (void)webView:(WKWebView*)theWebView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error { - [self updateButton:theWebView]; - - [self.spinner stopAnimating]; - - self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + [self webView:theWebView failedNavigation:@"didFailNavigation" withError:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailProvisionalNavigation" withError:error]; +} - [self.navigationDelegate webView:theWebView didFailLoadWithError: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]; } -- (void)updateButton:(UIWebView*)theWebView + +- (void)updateButton:(WKWebView*)theWebView { if (self.backButton) { self.backButton.enabled = _browserOptions.backButtonCanClose || theWebView.canGoBack; } - + if (self.forwardButton) { self.forwardButton.enabled = theWebView.canGoForward; } } -/** - * The reason why this method exists at all is because UIWebView is quite - * terrible with dealing this hash change, which IS a history change. However - * when moving to a new hash, only shouldStartLoadWithRequest will be called. - * Even then it's being called too early such that canGoback and canGoForward - * hasn't been updated yet. What makes it worse is that when navigating history - * involving hash by goBack and goForward, no callback is called at all, so we - * will have to depend on the back and forward button to give us hints when to - * change button states. - */ -- (void)updateButtonDelayed:(UIWebView*)theWebView -{ - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self updateButton:theWebView]; - }); -} #pragma mark CDVScreenOrientationDelegate @@ -1520,45 +1769,37 @@ - (BOOL)shouldAutorotate return YES; } -- (NSUInteger)supportedInterfaceOrientations +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; } - + return 1 << UIInterfaceOrientationPortrait; } -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; -} + (UIColor *)colorFromRGBA:(NSString *)rgba { unsigned rgbaVal = 0; - + if ([[rgba substringWithRange:NSMakeRange(0, 1)] isEqualToString:@"#"]) { // First char is #, get rid of that. rgba = [rgba substringFromIndex:1]; } - + if (rgba.length < 8) { // If alpha is not given, just append ff. rgba = [NSString stringWithFormat:@"%@ff", rgba]; } - + NSScanner *scanner = [NSScanner scannerWithString:rgba]; [scanner setScanLocation:0]; [scanner scanHexInt:&rgbaVal]; - + return [UIColor colorWithRed:(rgbaVal >> 24 & 0xFF) / 255.0f - green:(rgbaVal >> 16 & 0xFF) / 255.0f - blue:(rgbaVal >> 8 & 0xFF) / 255.0f - alpha:(rgbaVal & 0xFF) / 255.0f]; + green:(rgbaVal >> 16 & 0xFF) / 255.0f + blue:(rgbaVal >> 8 & 0xFF) / 255.0f + alpha:(rgbaVal & 0xFF) / 255.0f]; } @end @@ -1574,7 +1815,7 @@ - (id)init self.toolbarposition = kThemeableBrowserToolbarBarPositionBottom; self.clearcache = NO; self.clearsessioncache = NO; - + self.zoom = YES; self.mediaplaybackrequiresuseraction = NO; self.allowinlinemediaplayback = NO; @@ -1582,7 +1823,7 @@ - (id)init self.suppressesincrementalrendering = NO; self.hidden = NO; self.disallowoverscroll = NO; - + self.statusbar = nil; self.toolbar = nil; self.title = nil; @@ -1594,7 +1835,7 @@ - (id)init self.disableAnimation = NO; self.fullscreen = NO; } - + return self; } @@ -1604,6 +1845,12 @@ - (id)init @implementation CDVThemeableBrowserNavigationController : UINavigationController +- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion { + if ( self.presentedViewController) { + [super dismissViewControllerAnimated:flag completion:completion]; + } +} + - (BOOL)shouldAutorotate { if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { @@ -1612,23 +1859,15 @@ - (BOOL)shouldAutorotate return YES; } -- (NSUInteger)supportedInterfaceOrientations +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; } - + return 1 << UIInterfaceOrientationPortrait; } -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { - return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; - } - - return YES; -} - @end + diff --git a/src/ios/CDVThemeableBrowserUIDelegate.h b/src/ios/CDVThemeableBrowserUIDelegate.h new file mode 100644 index 000000000..2122ab757 --- /dev/null +++ b/src/ios/CDVThemeableBrowserUIDelegate.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 CDVThemeableBrowserUIDelegate : NSObject { + @private + UIViewController* _viewController; +} + +@property (nonatomic, copy) NSString* title; + +- (instancetype)initWithTitle:(NSString*)title; +-(void) setViewController:(UIViewController*) viewController; + +@end diff --git a/src/ios/CDVThemeableBrowserUIDelegate.m b/src/ios/CDVThemeableBrowserUIDelegate.m new file mode 100644 index 000000000..1a28dbfd5 --- /dev/null +++ b/src/ios/CDVThemeableBrowserUIDelegate.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 "CDVThemeableBrowserUIDelegate.h" + +@implementation CDVThemeableBrowserUIDelegate + +- (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/www/themeablebrowser.js b/www/themeablebrowser.js index a6931bf4d..5d77b3ce9 100644 --- a/www/themeablebrowser.js +++ b/www/themeablebrowser.js @@ -25,7 +25,13 @@ var modulemapper = require('cordova/modulemapper'); var urlutil = require('cordova/urlutil'); function ThemeableBrowser() { - this.channels = {}; + this.channels = { + 'loadstart': channel.create('loadstart'), + 'loadstop' : channel.create('loadstop'), + 'loaderror' : channel.create('loaderror'), + 'exit' : channel.create('exit'), + 'message' : channel.create('message') + }; } ThemeableBrowser.prototype = {