From 3052ddd7966fc666e54cb0cb824586e0cbcaf2b4 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 23 Jan 2018 10:25:31 +0000 Subject: [PATCH 01/27] Customisations --- README.md | 6 +- package.json | 19 +- src/android/ThemeableBrowser.java | 91 +++++- src/ios/CDVThemeableBrowser.m | 461 +++++++++++++++--------------- 4 files changed, 344 insertions(+), 233 deletions(-) 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..3127c9379 100644 --- a/package.json +++ b/package.json @@ -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/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 673e0d828..635d897dc 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -49,6 +49,8 @@ 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.ValueCallback; +import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -66,9 +68,11 @@ 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.apache.cordova.inappbrowser.InAppBrowser; import org.json.JSONException; import org.json.JSONObject; @@ -109,6 +113,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. * @@ -757,7 +766,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) { @@ -1184,6 +1237,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 */ diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index ce09b20ce..eb91e353b 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 @@ -86,7 +86,7 @@ - (CDVThemeableBrowser*)initWithWebView:(UIWebView*)theWebView _framesOpened = 0; _callbackIdPattern = nil; } - + return self; } #endif @@ -109,29 +109,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 +139,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 +153,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 +173,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 +183,7 @@ - (CDVThemeableBrowserOptions*)parseOptions:(NSString*)options JSONObjectWithData:data options:0 error:&error]; - + if(error) { [self emitError:kThemeableBrowserEmitCodeCritical withMessage:[NSString stringWithFormat:@"Invalid JSON %@", error]]; @@ -197,23 +197,23 @@ - (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]; @@ -224,7 +224,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } } - + if (browserOptions.clearsessioncache) { NSHTTPCookie *cookie; NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; @@ -235,7 +235,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } } - + if (self.themeableBrowserViewController == nil) { NSString* originalUA = [CDVUserAgentUtil originalUserAgent]; self.themeableBrowserViewController = [[CDVThemeableBrowserViewController alloc] @@ -243,12 +243,12 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options browserOptions: browserOptions navigationDelete:self statusBarStyle:[UIApplication sharedApplication].statusBarStyle]; - + 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 +264,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 +275,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,7 +288,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } } - + // UIWebView options self.themeableBrowserViewController.webView.scalesPageToFit = browserOptions.zoom; self.themeableBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; @@ -297,7 +297,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options 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 +321,24 @@ - (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; // 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]; + CGRect frame = [[UIScreen mainScreen] bounds]; + UIWindow *tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + UIViewController *tmpController = [[UIViewController alloc] init]; + [tmpWindow setRootViewController:tmpController]; + [tmpWindow setWindowLevel:UIWindowLevelNormal]; + + [tmpWindow makeKeyAndVisible]; + [tmpController presentViewController:nav animated:YES completion:nil]; } }); } @@ -339,7 +346,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 @@ -378,7 +385,7 @@ - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper // 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)"]; } - + if (jsWrapper != nil) { NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; @@ -395,7 +402,7 @@ - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper - (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]; } @@ -405,7 +412,7 @@ - (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]; } else { @@ -417,7 +424,7 @@ - (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]; } else { @@ -429,7 +436,7 @@ - (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]; } else { @@ -474,17 +481,17 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* { 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]; @@ -501,39 +508,39 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* 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; + // 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) { // 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 @@ -542,7 +549,7 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* // 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; } @@ -560,11 +567,11 @@ - (void)webViewDidFinishLoad:(UIWebView*)theWebView 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]; } } @@ -576,7 +583,7 @@ - (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error 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]; } } @@ -596,7 +603,7 @@ - (void)browserExit self.themeableBrowserViewController = nil; self.callbackId = nil; self.callbackIdPattern = nil; - + _framesOpened = 0; _isShown = NO; } @@ -607,7 +614,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 +622,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]; } @@ -658,14 +665,14 @@ - (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAg _statusBarStyle = statusBarStyle; [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; @@ -674,15 +681,15 @@ - (void)createViews webViewBounds.size.height -= toolbarHeight; } self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; - + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); - + [self.view addSubview:self.webView]; [self.view sendSubviewToBack:self.webView]; - + self.webView.delegate = _webViewDelegate; self.webView.backgroundColor = [UIColor whiteColor]; - + self.webView.clearsContextBeforeDrawing = YES; self.webView.clipsToBounds = YES; self.webView.contentMode = UIViewContentModeScaleToFill; @@ -690,7 +697,7 @@ - (void)createViews self.webView.opaque = YES; self.webView.scalesPageToFit = NO; self.webView.userInteractionEnabled = YES; - + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; self.spinner.alpha = 1.000; self.spinner.autoresizesSubviews = YES; @@ -705,10 +712,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); - + self.toolbar = [[UIView alloc] initWithFrame:toolbarFrame]; self.toolbar.alpha = 1.000; self.toolbar.autoresizesSubviews = YES; @@ -721,12 +728,12 @@ - (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]]; + if (image) { self.toolbar.backgroundColor = [UIColor colorWithPatternImage:image]; } else { @@ -736,10 +743,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 +760,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 +775,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 +800,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 +812,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 +821,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,24 +857,24 @@ - (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); // The correct positioning of title is not that important right now, since // rePositionViews will take care of it a bit later. @@ -878,14 +885,14 @@ - (void)createViews 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]; } - + [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]; @@ -917,7 +924,7 @@ - (UIImage*) getImage:(NSString*) name altPath:(NSString*) altPath altDensity:(C NSData* data = [NSData dataWithContentsOfFile:path]; result = [UIImage imageWithData:data scale:altDensity]; } - + return result; } @@ -928,9 +935,9 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe UIImage *buttonImage = nil; if (buttonProps[kThemeableBrowserPropImage] || buttonProps[kThemeableBrowserPropWwwImage]) { buttonImage = [self getImage:buttonProps[kThemeableBrowserPropImage] - altPath:buttonProps[kThemeableBrowserPropWwwImage] - altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]]; - + altPath:buttonProps[kThemeableBrowserPropWwwImage] + altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]]; + if (!buttonImage) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail withMessage:[NSString stringWithFormat:@"Image for %@, %@, failed to load.", @@ -940,15 +947,15 @@ - (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]];; + if (!buttonImagePressed) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail withMessage:[NSString stringWithFormat:@"Pressed image for %@, %@, failed to load.", @@ -958,18 +965,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 +985,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,7 +1005,7 @@ - (void)layoutButtons { CGFloat screenWidth = CGRectGetWidth(self.view.frame); CGFloat toolbarHeight = self.toolbar.frame.size.height; - + // Layout leftButtons and rightButtons from outer to inner. CGFloat left = 0; for (UIButton* button in self.leftButtons) { @@ -1006,7 +1013,7 @@ - (void)layoutButtons button.frame = CGRectMake(left, floorf((toolbarHeight - size.height) / 2), size.width, size.height); left += size.width; } - + CGFloat right = 0; for (UIButton* button in self.rightButtons) { CGSize size = button.frame.size; @@ -1019,14 +1026,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]; @@ -1036,45 +1043,45 @@ - (void)showLocationBar:(BOOL)show { CGRect locationbarFrame = self.addressLabel.frame; CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - + 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) { @@ -1093,18 +1100,18 @@ - (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]; - + 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 @@ -1118,7 +1125,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 +1136,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; @@ -1172,14 +1179,14 @@ - (UIStatusBarStyle)preferredStatusBarStyle - (void)close { [self emitEventForButton:_browserOptions.closeButton]; - + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; self.currentURL = nil; - + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { [self.navigationDelegate browserExit]; } - + // Run later to avoid the "took a long time" log message. dispatch_async(dispatch_get_main_queue(), ^{ if ([self respondsToSelector:@selector(presentingViewController)]) { @@ -1188,7 +1195,7 @@ - (void)close [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil]; } }); - + } - (void)reload @@ -1199,7 +1206,7 @@ - (void)reload - (void)navigateTo:(NSURL*)url { NSURLRequest* request = [NSURLRequest requestWithURL:url]; - + if (_userAgentLockToken != 0) { [self.webView loadRequest:request]; } else { @@ -1214,7 +1221,7 @@ - (void)navigateTo:(NSURL*)url - (void)goBack:(id)sender { [self emitEventForButton:_browserOptions.backButton]; - + if (self.webView.canGoBack) { [self.webView goBack]; [self updateButtonDelayed:self.webView]; @@ -1226,7 +1233,7 @@ - (void)goBack:(id)sender - (void)goForward:(id)sender { [self emitEventForButton:_browserOptions.forwardButton]; - + [self.webView goForward]; [self updateButtonDelayed:self.webView]; } @@ -1241,7 +1248,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 +1259,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 +1283,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 +1298,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,7 +1326,7 @@ - (void)viewWillAppear:(BOOL)animated [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; } [self rePositionViews]; - + [super viewWillAppear:animated]; } @@ -1337,18 +1344,18 @@ - (float) getStatusBarOffset { - (void) rePositionViews { CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight; - + 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 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); } - + [self layoutButtons]; } @@ -1397,7 +1404,7 @@ - (void)emitEventForButton:(NSDictionary*)buttonProps withIndex:(NSNumber*)index 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"]; } @@ -1414,44 +1421,44 @@ - (void)emitEventForButton:(NSDictionary*)buttonProps withIndex:(NSNumber*)index - (void)webViewDidStartLoad:(UIWebView*)theWebView { // loading url, start spinner - + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); - + [self.spinner startAnimating]; - + return [self.navigationDelegate webViewDidStartLoad:theWebView]; } - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; - + if (isTopLevelNavigation) { self.currentURL = request.URL; } - + [self updateButtonDelayed:theWebView]; - + return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; } - (void)webViewDidFinishLoad:(UIWebView*)theWebView { // update url, stop spinner, update back/forward - + self.addressLabel.text = [self.currentURL absoluteString]; [self updateButton:theWebView]; - + 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.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: @@ -1467,18 +1474,18 @@ - (void)webViewDidFinishLoad:(UIWebView*)theWebView if (isPDF) { [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; } - + [self.navigationDelegate webViewDidFinishLoad:theWebView]; } - (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error { [self updateButton:theWebView]; - + [self.spinner stopAnimating]; - + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); - + [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; } @@ -1487,7 +1494,7 @@ - (void)updateButton:(UIWebView*)theWebView if (self.backButton) { self.backButton.enabled = _browserOptions.backButtonCanClose || theWebView.canGoBack; } - + if (self.forwardButton) { self.forwardButton.enabled = theWebView.canGoForward; } @@ -1525,7 +1532,7 @@ - (NSUInteger)supportedInterfaceOrientations if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; } - + return 1 << UIInterfaceOrientationPortrait; } @@ -1534,31 +1541,31 @@ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interface 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 +1581,7 @@ - (id)init self.toolbarposition = kThemeableBrowserToolbarBarPositionBottom; self.clearcache = NO; self.clearsessioncache = NO; - + self.zoom = YES; self.mediaplaybackrequiresuseraction = NO; self.allowinlinemediaplayback = NO; @@ -1582,7 +1589,7 @@ - (id)init self.suppressesincrementalrendering = NO; self.hidden = NO; self.disallowoverscroll = NO; - + self.statusbar = nil; self.toolbar = nil; self.title = nil; @@ -1594,7 +1601,7 @@ - (id)init self.disableAnimation = NO; self.fullscreen = NO; } - + return self; } @@ -1604,6 +1611,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)]) { @@ -1617,7 +1630,7 @@ - (NSUInteger)supportedInterfaceOrientations if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; } - + return 1 << UIInterfaceOrientationPortrait; } @@ -1626,7 +1639,7 @@ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interface if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; } - + return YES; } From 7aa94d3b138f458d46ed444dd52987a0f022e932 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 27 Feb 2018 11:42:03 +0000 Subject: [PATCH 02/27] Bug fix: Webview below 20px under the toolbar on iOS 11. See https://github.com/initialxy/cordova-plugin-themeablebrowser/issues/168 --- src/ios/CDVThemeableBrowser.m | 56 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index eb91e353b..e3f009fa5 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1342,21 +1342,49 @@ - (float) getStatusBarOffset { } - (void) rePositionViews { - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight; - - 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 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); + // Webview height is a bug that appear in the plugin for ios >= 11 so we need to keep the previous code that work great for previous versions + if (@available(iOS 11, *)) { + + CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; + CGFloat statusBarOffset = [self getStatusBarOffset]; + CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight + statusBarOffset; + + if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { + // The webview height calculated did not take the status bar into account. Thus we need to remove status bar height to the webview height. + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height-statusBarOffset))]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } + // When positionning the iphone to landscape mode, status bar is hidden. The problem is that we set the webview height just before with removing the status bar height. We need to adjust the phenomen by adding the preview status bar height. We had to add manually 20 (pixel) because in landscape mode, the status bar height is equal to 0. + if (statusBarOffset == 0) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+20))]; + } + + 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); + } + + [self layoutButtons]; + + } else { + + CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; + CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight; + + 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 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); + } + + [self layoutButtons]; } - - [self layoutButtons]; } - (CGFloat) getFloatFromDict:(NSDictionary*)dict withKey:(NSString*)key withDefault:(CGFloat)def From 25756f5aa9d958dfccea7dcdcaeace078817f425 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 27 Feb 2018 11:42:44 +0000 Subject: [PATCH 03/27] Flag version as custom fork. --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3127c9379..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", diff --git a/plugin.xml b/plugin.xml index 9c4392d9a..f1b7f8c62 100644 --- a/plugin.xml +++ b/plugin.xml @@ -21,7 +21,7 @@ + version="0.2.17-custom"> ThemeableBrowser Cordova ThemeableBrowser Plugin From a543f591a706f11ae329b5a9c290d5a6f4143163 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Fri, 9 Mar 2018 12:05:28 +0000 Subject: [PATCH 04/27] Add Android support for postMessage API --- src/android/ThemeableBrowser.java | 26 +++++++++++++++++++++++++- www/themeablebrowser.js | 8 +++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 635d897dc..2f22b9b86 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -49,6 +49,7 @@ 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; @@ -72,7 +73,6 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.cordova.PluginManager; import org.apache.cordova.PluginResult; import org.apache.cordova.Whitelist; -import org.apache.cordova.inappbrowser.InAppBrowser; import org.json.JSONException; import org.json.JSONObject; @@ -94,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"; @@ -838,6 +839,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); @@ -1396,6 +1415,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); 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 = { From 5c051912b76f142b7bae2c545458c8122f6421af Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 6 Nov 2018 16:20:57 +0000 Subject: [PATCH 05/27] Add iOS support for postMessage API. Fix iOS crashes due to UIWebView delegate not being released on closing the IAB --- src/ios/CDVThemeableBrowser.m | 38 +++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index e3f009fa5..8d969bf1e 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -369,6 +369,19 @@ - (void)openInSystem:(NSURL*)url } } +-(void)createIframeBridge +{ + if (!_injectedIframeBridge) { + _injectedIframeBridge = YES; + // Create an iframe bridge in the new document to communicate with the CDVThemeableBrowserViewController + NSString* jsIframeBridge = @"var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);"; + // Add the postMessage API + NSString* jspostMessageApi = @"window.webkit={messageHandlers:{cordova_iab:{postMessage:function(message){_cdvIframeBridge.src='gap-iab://message/'+encodeURIComponent(message);}}}}"; + // Inject the JS to the webview + [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"(function(d){%@%@})(document)", jsIframeBridge, jspostMessageApi]]; + } +} + // This is a helper method for the inject{Script|Style}{Code|File} API calls, which // provides a consistent method for injecting JavaScript code into the document. // @@ -380,12 +393,6 @@ - (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)"]; - } - if (jsWrapper != nil) { NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; @@ -506,6 +513,22 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* } [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; return NO; + }else if ([scriptCallbackId isEqualToString:@"message"] && (self.callbackId != nil)) { + // Send a message event + NSString* scriptResult = [url path]; + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSError* __autoreleasing error = nil; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult 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]; + } + } } } else if ([self isSystemUrl:url]) { // Do not allow iTunes store links from ThemeableBrowser as they do not work @@ -561,6 +584,7 @@ - (void)webViewDidStartLoad:(UIWebView*)theWebView - (void)webViewDidFinishLoad:(UIWebView*)theWebView { + [self createIframeBridge]; 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]; @@ -1167,6 +1191,7 @@ - (void)viewDidLoad - (void)viewDidUnload { [self.webView loadHTMLString:nil baseURL:nil]; + self.webView.delegate = nil; [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; [super viewDidUnload]; } @@ -1182,6 +1207,7 @@ - (void)close [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; self.currentURL = nil; + self.webView.delegate = nil; if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { [self.navigationDelegate browserExit]; From e8dc67a12b1796ce547566e67b5d66bdb823bc33 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 11 Jun 2019 10:22:40 +0100 Subject: [PATCH 06/27] Add support for Cancel option in the list of menu items on Android (enables screenreader users to dismiss the popup menu). --- src/android/ThemeableBrowser.java | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 2f22b9b86..df3da3d93 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -81,6 +81,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 { @@ -707,6 +708,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(), @@ -1542,6 +1550,7 @@ private static class BrowserButton extends Event { private static class BrowserMenu extends BrowserButton { public EventLabel[] items; + public String cancel; } private static class Toolbar { @@ -1557,4 +1566,17 @@ private static class Title { public String staticText; public boolean showPageTitle; } + + 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; + } + } } From 09ce277ce19f29da4b29d6b8ef4f88469ea433fa Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 11 Jun 2019 13:24:35 +0100 Subject: [PATCH 07/27] Support custom accessibility descriptions for buttons on Android --- src/android/ThemeableBrowser.java | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index df3da3d93..9f3bc78c2 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -630,7 +630,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( @@ -653,7 +653,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( @@ -673,7 +673,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( @@ -684,13 +684,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 @@ -746,6 +752,7 @@ public void onNothingSelected( } } + // Title final TextView title = features.title != null ? new TextView(cordova.getActivity()) : null; @@ -1213,12 +1220,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); @@ -1228,7 +1235,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; } @@ -1546,6 +1553,7 @@ 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 { From 5dd23b88e8bc6fbe17c0af757009666b876f36bf Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Wed, 12 Jun 2019 13:46:06 +0100 Subject: [PATCH 08/27] Support custom accessibility descriptions for buttons on iOS --- src/ios/CDVThemeableBrowser.m | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 8d969bf1e..404b4076f 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -46,6 +46,7 @@ Licensed to the Apache Software Foundation (ASF) under one #define kThemeableBrowserPropTitle @"title" #define kThemeableBrowserPropCancel @"cancel" #define kThemeableBrowserPropItems @"items" +#define kThemeableBrowserPropAccessibilityDescription @"accessibilityDescription" #define kThemeableBrowserEmitError @"ThemeableBrowserError" #define kThemeableBrowserEmitWarning @"ThemeableBrowserWarning" @@ -514,7 +515,7 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; return NO; }else if ([scriptCallbackId isEqualToString:@"message"] && (self.callbackId != nil)) { - // Send a message event + // Send a message event NSString* scriptResult = [url path]; if ((scriptResult != nil) && ([scriptResult length] > 1)) { scriptResult = [scriptResult substringFromIndex:1]; @@ -525,7 +526,7 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* [dResult setValue:@"message" forKey:@"type"]; [dResult setObject:decodedResult forKey:@"data"]; CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dResult]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; } } @@ -756,7 +757,8 @@ - (void)createViews if (toolbarProps[kThemeableBrowserPropImage] || toolbarProps[kThemeableBrowserPropWwwImage]) { UIImage *image = [self getImage:toolbarProps[kThemeableBrowserPropImage] altPath:toolbarProps[kThemeableBrowserPropWwwImage] - altDensity:[toolbarProps[kThemeableBrowserPropWwwImageDensity] doubleValue]]; + altDensity:[toolbarProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription:@""]; if (image) { self.toolbar.backgroundColor = [UIColor colorWithPatternImage:image]; @@ -934,7 +936,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) { @@ -947,6 +949,8 @@ - (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; @@ -957,10 +961,16 @@ - (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]]; + altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription: accessibilityDescription + ]; if (!buttonImage) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail @@ -978,7 +988,9 @@ - (UIButton*) createButton:(NSDictionary*) buttonProps action:(SEL)action withDe if (buttonProps[kThemeableBrowserPropImagePressed] || buttonProps[kThemeableBrowserPropWwwImagePressed]) { buttonImagePressed = [self getImage:buttonProps[kThemeableBrowserPropImagePressed] altPath:buttonProps[kThemeableBrowserPropWwwImagePressed] - altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue]];; + altDensity:[buttonProps[kThemeableBrowserPropWwwImageDensity] doubleValue] + accessibilityDescription: accessibilityDescription + ];; if (!buttonImagePressed) { [self.navigationDelegate emitError:kThemeableBrowserEmitCodeLoadFail @@ -1699,3 +1711,4 @@ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interface @end + From 677888307f7854f68011929d0660f1ab370e189b Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 26 Sep 2019 10:44:27 +0100 Subject: [PATCH 09/27] Fixes for iOS 13 --- src/ios/CDVThemeableBrowser.h | 1 + src/ios/CDVThemeableBrowser.m | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index ed59e22f1..8294468d1 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -63,6 +63,7 @@ @class CDVThemeableBrowserViewController; @interface CDVThemeableBrowser : CDVPlugin { + UIWindow * tmpWindow; BOOL _injectedIframeBridge; } diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 404b4076f..5fd9fa0bc 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -329,16 +329,27 @@ - (void)show:(CDVInvokedUrlCommand*)command withAnimation:(BOOL)animated initWithRootViewController:self.themeableBrowserViewController]; nav.orientationDelegate = self.themeableBrowserViewController; nav.navigationBarHidden = YES; + if (@available(iOS 13.0, *)) { + nav.modalInPresentation = true; + 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) { - CGRect frame = [[UIScreen mainScreen] bounds]; - UIWindow *tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf->tmpWindow) { + CGRect frame = [[UIScreen mainScreen] bounds]; + strongSelf->tmpWindow = [[UIWindow alloc] initWithFrame:frame]; + } + UIViewController *tmpController = [[UIViewController alloc] init]; - [tmpWindow setRootViewController:tmpController]; - [tmpWindow setWindowLevel:UIWindowLevelNormal]; + [strongSelf->tmpWindow setRootViewController:tmpController]; + - [tmpWindow makeKeyAndVisible]; + [strongSelf->tmpWindow makeKeyAndVisible]; [tmpController presentViewController:nav animated:YES completion:nil]; } }); @@ -626,6 +637,8 @@ - (void)browserExit // 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; From 67bd29f22c7321e0d3a3740d203d4262eebf6a07 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 26 Sep 2019 12:23:22 +0100 Subject: [PATCH 10/27] Fix hide animation on iOS 13 --- src/ios/CDVThemeableBrowser.m | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 5fd9fa0bc..28690d668 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -330,7 +330,6 @@ - (void)show:(CDVInvokedUrlCommand*)command withAnimation:(BOOL)animated nav.orientationDelegate = self.themeableBrowserViewController; nav.navigationBarHidden = YES; if (@available(iOS 13.0, *)) { - nav.modalInPresentation = true; nav.modalPresentationStyle = UIModalPresentationOverFullScreen; } @@ -624,6 +623,13 @@ - (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error } } +- (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)browserExit { if (self.callbackId != nil) { @@ -1234,19 +1240,25 @@ - (void)close self.currentURL = nil; self.webView.delegate = nil; - if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { - [self.navigationDelegate browserExit]; - } + UIWindow* tmpWindow = [self.navigationDelegate getTmpWindow]; // 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:^{ + tmpWindow.hidden = YES; + }]; } else { - [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:nil]; + [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:^{ + tmpWindow.hidden = YES; + }]; } }); + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } + } - (void)reload From fb47d804c3e89b088556363753270ec3099db572 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 1 Oct 2019 11:03:51 +0100 Subject: [PATCH 11/27] Fix issue where, on building using Xcode 11 (iOS 13 SDK), after closing IAB and rotating device to landscape, main Cordova Webview is pushed down by the height of the status bar causing HTML content of the webview which is aligned to absolute bottom to be pushed off the bottom of the screen. --- src/ios/CDVThemeableBrowser.m | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 28690d668..8f664dd75 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -630,6 +630,10 @@ - (UIWindow*)getTmpWindow return self->tmpWindow; } +- (void) nilTmpWindow{ + self->tmpWindow = nil; +} + - (void)browserExit { if (self.callbackId != nil) { @@ -1239,18 +1243,17 @@ - (void)close [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; self.currentURL = nil; self.webView.delegate = nil; - - UIWindow* tmpWindow = [self.navigationDelegate getTmpWindow]; + 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:^{ - tmpWindow.hidden = YES; + [navigationDelegate nilTmpWindow]; }]; } else { [[self parentViewController] dismissViewControllerAnimated:!_browserOptions.disableAnimation completion:^{ - tmpWindow.hidden = YES; + [navigationDelegate nilTmpWindow]; }]; } }); From c20893d71eaadcfa06806c587af4e7b369ddd37e Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Fri, 25 Oct 2019 16:46:18 +0100 Subject: [PATCH 12/27] [iOS] Fix full screen display on iPhone X-family. Add extra options: title.fontSize, toolbar.paddingX --- src/ios/CDVThemeableBrowser.h | 4 +- src/ios/CDVThemeableBrowser.m | 110 ++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index 8294468d1..38b0acb32 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -113,7 +113,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; diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 8f664dd75..a31833320 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -44,9 +44,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" @@ -237,13 +240,25 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } } + 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 navigationDelete:self - statusBarStyle:[UIApplication sharedApplication].statusBarStyle]; + statusBarStyle:statusBarStyle]; if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { self.themeableBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; @@ -924,7 +939,14 @@ - (void)createViews [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; @@ -939,6 +961,11 @@ - (void)createViews 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]; } @@ -1064,19 +1091,22 @@ - (void)layoutButtons { CGFloat screenWidth = CGRectGetWidth(self.view.frame); CGFloat toolbarHeight = self.toolbar.frame.size.height; + CGFloat toolbarPadding = _browserOptions.fullscreen ? [self getStatusBarOffset] : 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; } } @@ -1233,7 +1263,18 @@ - (void)viewDidUnload - (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; } - (void)close @@ -1409,48 +1450,51 @@ - (float) getStatusBarOffset { - (void) rePositionViews { // Webview height is a bug that appear in the plugin for ios >= 11 so we need to keep the previous code that work great for previous versions + CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; + CGFloat statusBarOffset = [self getStatusBarOffset]; + CGFloat toolbarOffset = _browserOptions.fullscreen ? 0.0 : statusBarOffset; + CGFloat toolbarPadding = _browserOptions.fullscreen ? statusBarOffset : 0.0; + if (@available(iOS 11, *)) { - - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - CGFloat statusBarOffset = [self getStatusBarOffset]; - CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight + statusBarOffset; + // iOS 11+ + CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight : toolbarHeight + statusBarOffset; + CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarHeight == statusBarOffset ? statusBarOffset+10 : statusBarOffset+toolbarHeight) : -(toolbarOffset+toolbarPadding); if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { // The webview height calculated did not take the status bar into account. Thus we need to remove status bar height to the webview height. - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height-statusBarOffset))]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+webviewHeightOffset))]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, toolbarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height + toolbarPadding)]; } // When positionning the iphone to landscape mode, status bar is hidden. The problem is that we set the webview height just before with removing the status bar height. We need to adjust the phenomen by adding the preview status bar height. We had to add manually 20 (pixel) because in landscape mode, the status bar height is equal to 0. if (statusBarOffset == 0) { [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+20))]; } - 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); - } - - [self layoutButtons]; - } else { - - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - CGFloat webviewOffset = _browserOptions.fullscreen ? 0.0 : toolbarHeight; - + // iOS <=10 + CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight - statusBarOffset : toolbarHeight; + CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarOffset+toolbarPadding+statusBarOffset) : 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)]; + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, self.webView.frame.size.height+webviewHeightOffset)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, toolbarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height+toolbarPadding)]; } - + } + + if (self.titleLabel) { 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); + 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; } - - [self layoutButtons]; + self.titleLabel.frame = CGRectMake(leftOffset, toolbarPadding/2, width, toolbarHeight+(toolbarPadding/2)); } + + [self layoutButtons]; } - (CGFloat) getFloatFromDict:(NSDictionary*)dict withKey:(NSString*)key withDefault:(CGFloat)def From 9fed66d14bbd3276d84a4df09524880664f0decc Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 29 Oct 2019 10:15:06 +0000 Subject: [PATCH 13/27] [Android] Add extra options: title.fontSize, toolbar.paddingX --- src/android/ThemeableBrowser.java | 37 +++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 9f3bc78c2..c08033598 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -407,6 +407,7 @@ public void run() { // NB: wait for about:blank before dismissing public void onPageFinished(WebView view, String url) { if (dialog != null) { + int myWidth = dialog.getWindow().getDecorView().getWidth(); dialog.dismiss(); } @@ -569,10 +570,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)) { @@ -771,6 +774,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 @@ -997,10 +1003,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) { @@ -1027,6 +1053,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) { @@ -1567,12 +1594,14 @@ 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 { From 8f9daa3dbc19f7eb70c526a850da471b57e77fc0 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Fri, 8 Nov 2019 10:18:08 +0000 Subject: [PATCH 14/27] [iOS] Fix webview height when fullscreen=false --- src/ios/CDVThemeableBrowser.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index a31833320..4f7373181 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1458,7 +1458,7 @@ - (void) rePositionViews { if (@available(iOS 11, *)) { // iOS 11+ CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight : toolbarHeight + statusBarOffset; - CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarHeight == statusBarOffset ? statusBarOffset+10 : statusBarOffset+toolbarHeight) : -(toolbarOffset+toolbarPadding); + CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarHeight == statusBarOffset ? statusBarOffset+10 : statusBarOffset+toolbarHeight-statusBarOffset+10) : -(toolbarOffset+toolbarPadding-statusBarOffset+10); if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { // The webview height calculated did not take the status bar into account. Thus we need to remove status bar height to the webview height. @@ -1472,8 +1472,8 @@ - (void) rePositionViews { } else { // iOS <=10 - CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight - statusBarOffset : toolbarHeight; - CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarOffset+toolbarPadding+statusBarOffset) : 0; + CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight - statusBarOffset: toolbarHeight; + CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarOffset+toolbarHeight) : 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+webviewHeightOffset)]; [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, toolbarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height+toolbarPadding)]; From 24254ee7983df26a09aab7805e24d554d635c862 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 6 Jan 2020 14:43:28 +0000 Subject: [PATCH 15/27] Add try/catch to prevent crashes when dismissing dialog --- src/android/ThemeableBrowser.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index c08033598..8f415fe88 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -407,15 +407,17 @@ public void run() { // NB: wait for about:blank before dismissing public void onPageFinished(WebView view, String url) { if (dialog != null) { - int myWidth = dialog.getWindow().getDecorView().getWidth(); - 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; } }); From fb5d21e9d41834e4091e27a146dff96e4a2d4396 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 8 Jun 2020 12:16:38 +0100 Subject: [PATCH 16/27] WIP: Rework ThemeableBrowser to use WKWebView (fullscreen not working) --- src/ios/CDVThemeableBrowser.h | 19 +- src/ios/CDVThemeableBrowser.m | 545 ++++++++++++++++++++-------------- 2 files changed, 325 insertions(+), 239 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index 38b0acb32..44d0c14a6 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 {} @@ -67,6 +62,7 @@ BOOL _injectedIframeBridge; } +@property (nonatomic, retain) CDVThemeableBrowser* instance; @property (nonatomic, retain) CDVThemeableBrowserViewController* themeableBrowserViewController; @property (nonatomic, copy) NSString* callbackId; @property (nonatomic, copy) NSRegularExpression *callbackIdPattern; @@ -81,7 +77,7 @@ @end -@interface CDVThemeableBrowserViewController : UIViewController { +@interface CDVThemeableBrowserViewController : UIViewController { @private NSString* _userAgent; NSString* _prevUserAgent; @@ -89,15 +85,11 @@ UIStatusBarStyle _statusBarStyle; CDVThemeableBrowserOptions *_browserOptions; -#ifdef __CORDOVA_4_0_0 - CDVUIWebViewDelegate* _webViewDelegate; -#else - CDVWebViewDelegate* _webViewDelegate; -#endif } -@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; @@ -106,6 +98,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; diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 4f7373181..3f693e6af 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -21,6 +21,10 @@ Licensed to the Apache Software Foundation (ASF) under one #import #import +#if __has_include("CDVWKProcessPoolFactory.h") +#import "CDVWKProcessPoolFactory.h" +#endif + #define kThemeableBrowserTargetSelf @"_self" #define kThemeableBrowserTargetSystem @"_system" #define kThemeableBrowserTargetBlank @"_blank" @@ -28,6 +32,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" @@ -58,7 +64,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)) @@ -218,27 +224,68 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options // 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]){ @@ -259,6 +306,7 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options browserOptions: browserOptions navigationDelete:self statusBarStyle:statusBarStyle]; + self.themeableBrowserViewController.navigationDelegate = self; if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { self.themeableBrowserViewController.orientationDelegate = (UIViewController *)self.viewController; @@ -305,15 +353,6 @@ - (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]; @@ -395,19 +434,6 @@ - (void)openInSystem:(NSURL*)url } } --(void)createIframeBridge -{ - if (!_injectedIframeBridge) { - _injectedIframeBridge = YES; - // Create an iframe bridge in the new document to communicate with the CDVThemeableBrowserViewController - NSString* jsIframeBridge = @"var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);"; - // Add the postMessage API - NSString* jspostMessageApi = @"window.webkit={messageHandlers:{cordova_iab:{postMessage:function(message){_cdvIframeBridge.src='gap-iab://message/'+encodeURIComponent(message);}}}}"; - // Inject the JS to the webview - [self.themeableBrowserViewController.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"(function(d){%@%@})(document)", jsIframeBridge, jspostMessageApi]]; - } -} - // This is a helper method for the inject{Script|Style}{Code|File} API calls, which // provides a consistent method for injecting JavaScript code into the document. // @@ -419,25 +445,43 @@ -(void)createIframeBridge - (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper { + // 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]; } @@ -447,7 +491,7 @@ - (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)"; } @@ -459,7 +503,7 @@ - (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)"; } @@ -471,7 +515,7 @@ - (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)"; } @@ -496,92 +540,24 @@ - (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]]; +- (void)webView:(WKWebView *)theWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - // 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 ([scriptCallbackId isEqualToString:@"message"] && (self.callbackId != nil)) { - // Send a message event - NSString* scriptResult = [url path]; - if ((scriptResult != nil) && ([scriptResult length] > 1)) { - scriptResult = [scriptResult substringFromIndex:1]; - NSError* __autoreleasing error = nil; - NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult 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]; - } - } - } - } 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) { + 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]}]; @@ -590,46 +566,94 @@ - (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest* [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; + 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 { - return YES; + 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 { - [self createIframeBridge]; 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]]; @@ -657,6 +681,16 @@ - (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. @@ -711,6 +745,7 @@ - (void)emitWarning:(NSString*)code withMessage:(NSString*)message @implementation CDVThemeableBrowserViewController @synthesize currentURL; +CGFloat lastReducedStatusBarHeight = 0.0; - (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle { @@ -719,11 +754,8 @@ - (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAg _userAgent = userAgent; _prevUserAgent = prevUserAgent; _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; [self createViews]; @@ -739,18 +771,42 @@ - (void)createViews 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]; + CGFloat toolbarHeight = [self getFloatFromDict:toolbarProps withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; if (!_browserOptions.fullscreen) { webViewBounds.size.height -= toolbarHeight; } - self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; + 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 = [[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; @@ -758,8 +814,17 @@ - (void)createViews 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; @@ -1131,7 +1196,7 @@ - (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 getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; BOOL toolbarVisible = !self.toolbar.hidden; @@ -1188,7 +1253,7 @@ - (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 getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; BOOL locationbarVisible = !self.addressLabel.hidden; @@ -1256,11 +1321,16 @@ - (void)viewDidLoad - (void)viewDidUnload { [self.webView loadHTMLString:nil baseURL:nil]; - self.webView.delegate = nil; + self.webView.UIDelegate = nil; [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; [super viewDidUnload]; } +- (void) viewDidDisappear:(BOOL)animated +{ + lastReducedStatusBarHeight = 0; +} + - (UIStatusBarStyle)preferredStatusBarStyle { UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; @@ -1283,7 +1353,7 @@ - (void)close [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; self.currentURL = nil; - self.webView.delegate = nil; + self.webView.UIDelegate = nil; CDVThemeableBrowser* navigationDelegate = self.navigationDelegate; // Run later to avoid the "took a long time" log message. @@ -1443,43 +1513,34 @@ - (void)viewWillAppear:(BOOL)animated // 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; + return (float) IsAtLeastiOSVersion(@"7.0") ? [[UIApplication sharedApplication] statusBarFrame].size.height : 0.0; } - (void) rePositionViews { - // Webview height is a bug that appear in the plugin for ios >= 11 so we need to keep the previous code that work great for previous versions - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_DEF_HEIGHT]; - CGFloat statusBarOffset = [self getStatusBarOffset]; - CGFloat toolbarOffset = _browserOptions.fullscreen ? 0.0 : statusBarOffset; - CGFloat toolbarPadding = _browserOptions.fullscreen ? statusBarOffset : 0.0; - - if (@available(iOS 11, *)) { - // iOS 11+ - CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight : toolbarHeight + statusBarOffset; - CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarHeight == statusBarOffset ? statusBarOffset+10 : statusBarOffset+toolbarHeight-statusBarOffset+10) : -(toolbarOffset+toolbarPadding-statusBarOffset+10); - - if ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]) { - // The webview height calculated did not take the status bar into account. Thus we need to remove status bar height to the webview height. - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+webviewHeightOffset))]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, toolbarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height + toolbarPadding)]; - } - // When positionning the iphone to landscape mode, status bar is hidden. The problem is that we set the webview height just before with removing the status bar height. We need to adjust the phenomen by adding the preview status bar height. We had to add manually 20 (pixel) because in landscape mode, the status bar height is equal to 0. - if (statusBarOffset == 0) { - [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, webviewOffset, self.webView.frame.size.width, (self.webView.frame.size.height+20))]; - } - - } else { - // iOS <=10 - CGFloat webviewOffset = _browserOptions.fullscreen ? toolbarPadding + toolbarHeight - statusBarOffset: toolbarHeight; - CGFloat webviewHeightOffset = _browserOptions.fullscreen ? -(toolbarOffset+toolbarHeight) : 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+webviewHeightOffset)]; - [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, toolbarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height+toolbarPadding)]; - } + + CGRect viewBounds = [self.webView bounds]; + CGFloat statusBarHeight = [self getStatusBarOffset]; + CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; + + // orientation portrait or portraitUpsideDown: status bar is on the top and web view is to be aligned to the bottom of the status bar + // orientation landscapeLeft or landscapeRight: status bar height is 0 in but lets account for it in case things ever change in the future + viewBounds.origin.y = statusBarHeight; + + // account for web view height portion that may have been reduced by a previous call to this method + viewBounds.size.height = viewBounds.size.height - statusBarHeight + lastReducedStatusBarHeight; + lastReducedStatusBarHeight = statusBarHeight; + + + if ((_browserOptions.toolbar) && ([_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop])) { + // if we have to display the toolbar on top of the web view, we need to account for its height + viewBounds.origin.y += toolbarHeight; + self.toolbar.frame = CGRectMake(self.toolbar.frame.origin.x, statusBarHeight, self.toolbar.frame.size.width, self.toolbar.frame.size.height); } + self.webView.frame = viewBounds; + + + CGFloat toolbarPadding = _browserOptions.fullscreen ? statusBarHeight : 0.0; if (self.titleLabel) { CGFloat screenWidth = CGRectGetWidth(self.view.frame); NSInteger width = floorf(screenWidth - (self.titleOffsetLeft + self.titleOffsetRight)); @@ -1497,6 +1558,19 @@ - (void) rePositionViews { [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; @@ -1554,46 +1628,41 @@ - (void)emitEventForButton:(NSDictionary*)buttonProps withIndex:(NSNumber*)index } } -#pragma mark UIWebViewDelegate +#pragma mark WKNavigationDelegate -- (void)webViewDidStartLoad:(UIWebView*)theWebView -{ - // loading url, start spinner +- (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigation *)navigation{ - self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + // loading url, start spinner, update back/forward - [self.spinner startAnimating]; + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; - return [self.navigationDelegate webViewDidStartLoad:theWebView]; + 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]; - - if (self.titleLabel && _browserOptions.title - && !_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.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + theWebView.scrollView.contentInset = UIEdgeInsetsZero; [self.spinner stopAnimating]; @@ -1608,25 +1677,49 @@ - (void)webViewDidFinishLoad:(UIWebView*)theWebView // 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"]]; + BOOL isPDF = NO; + //TODO webview class + //BOOL isPDF = [@"true" isEqualToString :[theWebView evaluateJavaScript:@"document.body==null"]]; if (isPDF) { [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; } - [self.navigationDelegate webViewDidFinishLoad:theWebView]; + [self.navigationDelegate didFinishNavigation:theWebView]; } - -- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error -{ - [self updateButton: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 didFailLoadWithError:error]; + [self.navigationDelegate webView:theWebView didFailNavigation:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailNavigation" withError:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error +{ + [self webView:theWebView failedNavigation:@"didFailProvisionalNavigation" withError:error]; } +#pragma mark WKScriptMessageHandler delegate +- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + if (![message.name isEqualToString:IAB_BRIDGE_NAME]) { + return; + } + //NSLog(@"Received script message %@", message.body); + [self.navigationDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + + - (void)updateButton:(UIWebView*)theWebView { if (self.backButton) { From c33da4ad8d652510ada61f29dbba17f6d0c3bc0b Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 16 Jun 2020 10:25:55 +0100 Subject: [PATCH 17/27] Hack to make notched vs unnotched heights correct in portrait only (does not work in apps that support landscape orientation) --- src/ios/CDVThemeableBrowser.m | 157 +++++++++++++++++++++------------- 1 file changed, 99 insertions(+), 58 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 3f693e6af..26816bfe3 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -771,34 +771,30 @@ - (void)createViews CGRect webViewBounds = self.view.bounds; BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kThemeableBrowserToolbarBarPositionTop]; NSDictionary* toolbarProps = _browserOptions.toolbar; - CGFloat toolbarHeight = [self getFloatFromDict:toolbarProps withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; - if (!_browserOptions.fullscreen) { - webViewBounds.size.height -= toolbarHeight; - } - 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; + 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; } - - - - self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; + }else{ // iOS 9 + configuration.mediaPlaybackRequiresUserAction = _browserOptions.mediaplaybackrequiresuseraction; + } + + self.webView = [[WKWebView alloc] initWithFrame:webViewBounds configuration:configuration]; self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); @@ -841,8 +837,8 @@ - (void)createViews 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; @@ -1016,7 +1012,7 @@ - (void)createViews // 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; @@ -1156,7 +1152,7 @@ - (void)layoutButtons { CGFloat screenWidth = CGRectGetWidth(self.view.frame); CGFloat toolbarHeight = self.toolbar.frame.size.height; - CGFloat toolbarPadding = _browserOptions.fullscreen ? [self getStatusBarOffset] : 0.0; + CGFloat toolbarPadding = _browserOptions.fullscreen ? [self getStatusBarHeight] : 0.0; // Layout leftButtons and rightButtons from outer to inner. CGFloat left = self.toolbarPaddingX; @@ -1196,7 +1192,7 @@ - (void)setCloseButtonTitle:(NSString*)title - (void)showLocationBar:(BOOL)show { CGRect locationbarFrame = self.addressLabel.frame; - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; + CGFloat toolbarHeight = [self getOffsetToolbarHeight]; BOOL toolbarVisible = !self.toolbar.hidden; @@ -1213,9 +1209,6 @@ - (void)showLocationBar:(BOOL)show // 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; @@ -1238,9 +1231,6 @@ - (void)showLocationBar:(BOOL)show // 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 @@ -1253,7 +1243,7 @@ - (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_HEIGHT]; + CGFloat toolbarHeight = [self getOffsetToolbarHeight]; BOOL locationbarVisible = !self.addressLabel.hidden; @@ -1269,9 +1259,6 @@ - (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition 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; @@ -1347,6 +1334,10 @@ - (UIStatusBarStyle)preferredStatusBarStyle return statusBarStyle; } +- (BOOL) prefersStatusBarHidden{ + return _browserOptions.fullscreen; +} + - (void)close { [self emitEventForButton:_browserOptions.closeButton]; @@ -1507,40 +1498,90 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; } -// -// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. -// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't -// change that value. -// -- (float) getStatusBarOffset { - return (float) IsAtLeastiOSVersion(@"7.0") ? [[UIApplication sharedApplication] statusBarFrame].size.height : 0.0; + +- (CGFloat) getStatusBarHeight { + return [[UIApplication sharedApplication] statusBarFrame].size.height; +} + +- (CGFloat) getStatusBarOffset { + CGFloat offset = 0; + if(_browserOptions.fullscreen){ + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + if(orientation != UIDeviceOrientationPortrait){ + offset = [self getTopSafeAreaInset]; + } + }else{ + offset = [self getStatusBarHeight]; + } + return offset; +} + +- (BOOL)hasTopNotch { + return [self getTopSafeAreaInset] > 20.0; +} + +- (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; +} + +- (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 statusBarHeight = [self getStatusBarOffset]; - CGFloat toolbarHeight = [self getFloatFromDict:_browserOptions.toolbar withKey:kThemeableBrowserPropHeight withDefault:TOOLBAR_HEIGHT]; + CGFloat statusBarHeight = [self getStatusBarHeight]; + CGFloat statusBarOffset = [self getStatusBarOffset]; + CGFloat toolbarHeight = [self getToolbarHeight]; + CGFloat toolbarTopSafeAreaOffset = [self getToolbarTopSafeAreaOffset]; + CGFloat toolbarOffsetHeight = [self getOffsetToolbarHeight]; // orientation portrait or portraitUpsideDown: status bar is on the top and web view is to be aligned to the bottom of the status bar // orientation landscapeLeft or landscapeRight: status bar height is 0 in but lets account for it in case things ever change in the future - viewBounds.origin.y = statusBarHeight; + 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 - statusBarHeight + lastReducedStatusBarHeight; - lastReducedStatusBarHeight = statusBarHeight; + viewBounds.size.height = viewBounds.size.height - statusBarOffset + (_browserOptions.fullscreen ? 0 : lastReducedStatusBarHeight); + lastReducedStatusBarHeight = statusBarOffset; 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 - viewBounds.origin.y += toolbarHeight; - self.toolbar.frame = CGRectMake(self.toolbar.frame.origin.x, statusBarHeight, self.toolbar.frame.size.width, self.toolbar.frame.size.height); + viewBounds.origin.y += toolbarOffsetHeight; + viewBounds.size.height -= toolbarOffsetHeight; + self.toolbar.frame = CGRectMake(self.toolbar.frame.origin.x, statusBarOffset, self.toolbar.frame.size.width, self.toolbar.frame.size.height); } self.webView.frame = viewBounds; - CGFloat toolbarPadding = _browserOptions.fullscreen ? statusBarHeight : 0.0; + if (self.titleLabel) { CGFloat screenWidth = CGRectGetWidth(self.view.frame); NSInteger width = floorf(screenWidth - (self.titleOffsetLeft + self.titleOffsetRight)); @@ -1552,7 +1593,7 @@ - (void) rePositionViews { }else{ leftOffset = self.toolbarPaddingX; } - self.titleLabel.frame = CGRectMake(leftOffset, toolbarPadding/2, width, toolbarHeight+(toolbarPadding/2)); + self.titleLabel.frame = CGRectMake(leftOffset, toolbarTopSafeAreaOffset, width, toolbarHeight); } [self layoutButtons]; From b00803ddaea63635bb07c471b0ba08b1aee019ac Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 16 Jun 2020 11:01:32 +0100 Subject: [PATCH 18/27] Add missing CDVThemeableBrowserUIDelegate --- plugin.xml | 2 + src/ios/CDVThemeableBrowserUIDelegate.h | 32 ++++++ src/ios/CDVThemeableBrowserUIDelegate.m | 127 ++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 src/ios/CDVThemeableBrowserUIDelegate.h create mode 100644 src/ios/CDVThemeableBrowserUIDelegate.m diff --git a/plugin.xml b/plugin.xml index f1b7f8c62..0574f174b 100644 --- a/plugin.xml +++ b/plugin.xml @@ -64,6 +64,8 @@ + + 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 From d3dfdd767158fe94769a7a7ccc796b094910d2b7 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 16 Jun 2020 13:57:57 +0100 Subject: [PATCH 19/27] Fix height in landscape and when orientation changes --- src/ios/CDVThemeableBrowser.h | 1 + src/ios/CDVThemeableBrowser.m | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index 44d0c14a6..62cbadb4b 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -83,6 +83,7 @@ NSString* _prevUserAgent; NSInteger _userAgentLockToken; UIStatusBarStyle _statusBarStyle; + CGFloat _initialStatusBarHeight; CDVThemeableBrowserOptions *_browserOptions; diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 26816bfe3..45c124e15 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -758,6 +758,7 @@ - (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAg [self.webViewUIDelegate setViewController:self]; _navigationDelegate = navigationDelegate; _statusBarStyle = statusBarStyle; + _initialStatusBarHeight = [self getStatusBarHeight]; [self createViews]; } @@ -1506,8 +1507,7 @@ - (CGFloat) getStatusBarHeight { - (CGFloat) getStatusBarOffset { CGFloat offset = 0; if(_browserOptions.fullscreen){ - UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; - if(orientation != UIDeviceOrientationPortrait){ + if(![self isPortrait]){ offset = [self getTopSafeAreaInset]; } }else{ @@ -1516,6 +1516,10 @@ - (CGFloat) getStatusBarOffset { return offset; } +- (BOOL) isPortrait{ + return [[UIDevice currentDevice] orientation] == UIDeviceOrientationPortrait; +} + - (BOOL)hasTopNotch { return [self getTopSafeAreaInset] > 20.0; } @@ -1556,11 +1560,9 @@ -(CGFloat) getToolbarHeight { - (void) rePositionViews { CGRect viewBounds = [self.webView bounds]; - CGFloat statusBarHeight = [self getStatusBarHeight]; CGFloat statusBarOffset = [self getStatusBarOffset]; CGFloat toolbarHeight = [self getToolbarHeight]; CGFloat toolbarTopSafeAreaOffset = [self getToolbarTopSafeAreaOffset]; - CGFloat toolbarOffsetHeight = [self getOffsetToolbarHeight]; // 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 @@ -1570,11 +1572,16 @@ - (void) rePositionViews { 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 - viewBounds.origin.y += toolbarOffsetHeight; - viewBounds.size.height -= toolbarOffsetHeight; + 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; From 4f10b6d010b8d863821b8629684fed74a56890dc Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 18 Jun 2020 11:08:15 +0100 Subject: [PATCH 20/27] Update/remove deprecations to enable compatibility with cordova-ios@6 --- src/ios/CDVThemeableBrowser.h | 7 +----- src/ios/CDVThemeableBrowser.m | 46 +++++------------------------------ 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index 62cbadb4b..681ce30b8 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -79,14 +79,9 @@ @interface CDVThemeableBrowserViewController : UIViewController { @private - NSString* _userAgent; - NSString* _prevUserAgent; - NSInteger _userAgentLockToken; UIStatusBarStyle _statusBarStyle; CGFloat _initialStatusBarHeight; CDVThemeableBrowserOptions *_browserOptions; - - } @property (nonatomic, strong) IBOutlet WKWebView* webView; @@ -118,7 +113,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 45c124e15..7f6ef4fca 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -19,7 +19,6 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVThemeableBrowser.h" #import -#import #if __has_include("CDVWKProcessPoolFactory.h") #import "CDVWKProcessPoolFactory.h" @@ -300,10 +299,8 @@ - (void)openInThemeableBrowser:(NSURL*)url withOptions:(NSString*)options } 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:statusBarStyle]; self.themeableBrowserViewController.navigationDelegate = self; @@ -747,12 +744,10 @@ @implementation CDVThemeableBrowserViewController @synthesize currentURL; CGFloat lastReducedStatusBarHeight = 0.0; -- (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; _browserOptions = browserOptions; self.webViewUIDelegate = [[CDVThemeableBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; [self.webViewUIDelegate setViewController:self]; @@ -1310,7 +1305,6 @@ - (void)viewDidUnload { [self.webView loadHTMLString:nil baseURL:nil]; self.webView.UIDelegate = nil; - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; [super viewDidUnload]; } @@ -1343,7 +1337,6 @@ - (void)close { [self emitEventForButton:_browserOptions.closeButton]; - [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; self.currentURL = nil; self.webView.UIDelegate = nil; CDVThemeableBrowser* navigationDelegate = self.navigationDelegate; @@ -1376,15 +1369,7 @@ - (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 @@ -1728,9 +1713,7 @@ - (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navi BOOL isPDF = NO; //TODO webview class //BOOL isPDF = [@"true" isEqualToString :[theWebView evaluateJavaScript:@"document.body==null"]]; - if (isPDF) { - [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; - } + [self.navigationDelegate didFinishNavigation:theWebView]; } @@ -1806,7 +1789,7 @@ - (BOOL)shouldAutorotate return YES; } -- (NSUInteger)supportedInterfaceOrientations +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; @@ -1815,14 +1798,6 @@ - (NSUInteger)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; @@ -1904,7 +1879,7 @@ - (BOOL)shouldAutorotate return YES; } -- (NSUInteger)supportedInterfaceOrientations +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { return [self.orientationDelegate supportedInterfaceOrientations]; @@ -1913,15 +1888,6 @@ - (NSUInteger)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 From 318c64565f67194804161b62df3661d8735c7bf4 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 18 Jun 2020 12:17:49 +0100 Subject: [PATCH 21/27] Make lastReducedStatusBarHeight private so it doesn't conflict with Cordova IAB (which contains it publicly) --- src/ios/CDVThemeableBrowser.h | 1 + src/ios/CDVThemeableBrowser.m | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.h b/src/ios/CDVThemeableBrowser.h index 681ce30b8..a862c9a6e 100644 --- a/src/ios/CDVThemeableBrowser.h +++ b/src/ios/CDVThemeableBrowser.h @@ -82,6 +82,7 @@ UIStatusBarStyle _statusBarStyle; CGFloat _initialStatusBarHeight; CDVThemeableBrowserOptions *_browserOptions; + CGFloat _lastReducedStatusBarHeight; } @property (nonatomic, strong) IBOutlet WKWebView* webView; diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 7f6ef4fca..0d1c5443f 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -742,12 +742,12 @@ - (void)emitWarning:(NSString*)code withMessage:(NSString*)message @implementation CDVThemeableBrowserViewController @synthesize currentURL; -CGFloat lastReducedStatusBarHeight = 0.0; - (id)init:(CDVThemeableBrowserOptions*) browserOptions navigationDelete:(CDVThemeableBrowser*) navigationDelegate statusBarStyle:(UIStatusBarStyle) statusBarStyle { self = [super init]; if (self != nil) { + _lastReducedStatusBarHeight = 0.0; _browserOptions = browserOptions; self.webViewUIDelegate = [[CDVThemeableBrowserUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; [self.webViewUIDelegate setViewController:self]; @@ -1310,7 +1310,7 @@ - (void)viewDidUnload - (void) viewDidDisappear:(BOOL)animated { - lastReducedStatusBarHeight = 0; + _lastReducedStatusBarHeight = 0; } - (UIStatusBarStyle)preferredStatusBarStyle @@ -1554,14 +1554,14 @@ - (void) rePositionViews { 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; + 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); + CGFloat webViewOffset = [self getToolbarHeight] + (_browserOptions.fullscreen || [self isPortrait] ? _initialStatusBarHeight : 0) + (_browserOptions.fullscreen ? _lastReducedStatusBarHeight : 0); viewBounds.origin.y = webViewOffset; CGFloat webViewHeight = initialWebViewHeight - webViewOffset; From 7bc2b18bc47b2ec7e0db4616feb6cb3dcc59a106 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 23 Jun 2020 09:53:45 +0100 Subject: [PATCH 22/27] Display document title as toolbar title if so configured --- src/ios/CDVThemeableBrowser.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 0d1c5443f..c96997d89 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1697,6 +1697,16 @@ - (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navi self.forwardButton.enabled = theWebView.canGoForward; theWebView.scrollView.contentInset = UIEdgeInsetsZero; + if (self.titleLabel && _browserOptions.title + && !_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.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 From 9438e7e36fa56dd5e21ecfd1236583af67a92d15 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 23 Jun 2020 10:41:52 +0100 Subject: [PATCH 23/27] Add missing call to start spinner --- src/ios/CDVThemeableBrowser.m | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index c96997d89..fef63f599 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1670,6 +1670,7 @@ - (void)webView:(WKWebView *)theWebView didStartProvisionalNavigation:(WKNavigat self.addressLabel.text = NSLocalizedString(@"Loading...", nil); self.backButton.enabled = theWebView.canGoBack; self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner startAnimating]; return [self.navigationDelegate didStartProvisionalNavigation:theWebView]; } From a1845f788dbff2c974031abe0f06717b6d7f5907 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 30 Jun 2020 13:20:40 +0100 Subject: [PATCH 24/27] Remove remaining references to UIWebView --- src/ios/CDVThemeableBrowser.m | 52 +++-------------------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index fef63f599..0cb048010 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -79,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 { @@ -1378,7 +1365,7 @@ - (void)goBack:(id)sender if (self.webView.canGoBack) { [self.webView goBack]; - [self updateButtonDelayed:self.webView]; + [self updateButton:self.webView]; } else if (_browserOptions.backButtonCanClose) { [self close]; } @@ -1389,7 +1376,7 @@ - (void)goForward:(id)sender [self emitEventForButton:_browserOptions.forwardButton]; [self.webView goForward]; - [self updateButtonDelayed:self.webView]; + [self updateButton:self.webView]; } - (void)goCustomButton:(id)sender @@ -1709,21 +1696,6 @@ - (void)webView:(WKWebView *)theWebView didFinishNavigation:(WKNavigation *)navi } [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 = NO; - //TODO webview class - //BOOL isPDF = [@"true" isEqualToString :[theWebView evaluateJavaScript:@"document.body==null"]]; [self.navigationDelegate didFinishNavigation:theWebView]; @@ -1762,7 +1734,7 @@ - (void)userContentController:(nonnull WKUserContentController *)userContentCont } -- (void)updateButton:(UIWebView*)theWebView +- (void)updateButton:(WKWebView*)theWebView { if (self.backButton) { self.backButton.enabled = _browserOptions.backButtonCanClose || theWebView.canGoBack; @@ -1773,22 +1745,6 @@ - (void)updateButton:(UIWebView*)theWebView } } -/** - * 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 From 3dff818d59e8da1b4cb818edb657fdf191f70cde Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 7 Sep 2020 16:28:40 +0100 Subject: [PATCH 25/27] (iOS) Fix vertical alignment of title when full screen --- src/ios/CDVThemeableBrowser.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index 0cb048010..bcf9f8873 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1572,7 +1572,13 @@ - (void) rePositionViews { }else{ leftOffset = self.toolbarPaddingX; } - self.titleLabel.frame = CGRectMake(leftOffset, toolbarTopSafeAreaOffset, width, toolbarHeight); + + 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]; From b74e0b56ecec3847ef8d44f5d1c0c3b15cb91d05 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Tue, 6 Jul 2021 10:38:48 +0100 Subject: [PATCH 26/27] Add Whitelist and WhitelistPlugin classes since the former has been removed from `cordova-android@10` and the latter is from `cordova-plugin-whitelist` which is incompatible with `cordova-android@10` --- plugin.xml | 2 + src/android/ThemeableBrowser.java | 1 - src/android/Whitelist.java | 170 ++++++++++++++++++++++++++++++ src/android/WhitelistPlugin.java | 160 ++++++++++++++++++++++++++++ 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/android/Whitelist.java create mode 100644 src/android/WhitelistPlugin.java diff --git a/plugin.xml b/plugin.xml index 0574f174b..5f115f51f 100644 --- a/plugin.xml +++ b/plugin.xml @@ -49,6 +49,8 @@ + + diff --git a/src/android/ThemeableBrowser.java b/src/android/ThemeableBrowser.java index 8f415fe88..c31990ec9 100644 --- a/src/android/ThemeableBrowser.java +++ b/src/android/ThemeableBrowser.java @@ -72,7 +72,6 @@ Licensed to the Apache Software Foundation (ASF) under one 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; 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; + } +} From 523bce8234470ed1c35d467f099f20e8a1d0eb00 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 14 Feb 2022 13:34:29 +0000 Subject: [PATCH 27/27] bugfix: Prevent nil url from being set on button event dictionary to prevent exception. Wrap emitEventForButton in try/catch to handle any exceptions and prevent crash. --- src/ios/CDVThemeableBrowser.m | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/ios/CDVThemeableBrowser.m b/src/ios/CDVThemeableBrowser.m index bcf9f8873..76e5ef93d 100644 --- a/src/ios/CDVThemeableBrowser.m +++ b/src/ios/CDVThemeableBrowser.m @@ -1636,21 +1636,28 @@ - (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"]; + @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"]; + 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); } }