This allows the WebView to access the internet within the app.
If you allow your customers to submit attachments through your ParqEx ticket form using the "Add File" button, you also need to add the following to your AndroidManifest file:
package com.example.myapplication
import android.content.pm.ApplicationInfo
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (0 != (applicationInfo.flags and
ApplicationInfo.FLAG_DEBUGGABLE)) {
WebView.setWebContentsDebuggingEnabled(true)
}
}
my_web_view.settings.javaScriptEnabled = true
my_web_view.settings.domStorageEnabled = true
my_web_view.settings.databaseEnabled = true
my_web_view.webViewClient = object : WebViewClient() {
}
my_web_view.loadUrl(BASE_URL)
}
companion object {
private val BASE_URL = "<YOUR WEB TICKET SUBMISSION URL WITH PARQEX>"
}
}
This is the basic code for opening your main ParqEx page (which should auto-launch ParqEx) in a webview. If ParqEx is not installed on your main page or does not auto-launch, contact your ParqEx Sales Engineer or Solutions Engineer or ParqEx support. Note: to customize the behavior of the webview in various ways, please consult the documentation.
Navigating back to ParqEx after opening a link to a KB article
Users have the ability to see the original KB articles that contain the solutions ParqEx returns by clicking on the "Read Article" link underneath each solution. On desktop, these links open in a new browser tab. On mobile, if you follow this guide, those links will open in the same webview. However, in this scenario, the user needs a way to navigate back to ParqEx. Add this code to enable the device Back button to trigger the "Back" navigation function in the webview:
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Check if the key event was the Back button and if there's history
if (keyCode == KeyEvent.KEYCODE_BACK && my_web_view.canGoBack()) {
my_web_view.goBack()
return true
}
// If it wasn't the Back key or there's no web page history, bubble up to the default
// system behavior (probably exit the activity)
return super.onKeyDown(keyCode, event)
}
Passing data to the webview
In certain situations, it is necessary to pass data to ParqEx running in the webview, e.g. to specify which language the app is using so ParqEx can be properly localized, or metadata (like mobile platform, app version, user ID, etc.). This can easily be accomplished by setting JS variables on the webpage when launching the webview. Put all your variables in the window.parqexConfig object. If you want the data to be passed directly as certain custom fields, please use the custom field ID as the variable name, as below:
my_web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
if (url == BASE_URL) {
my_web_view.loadUrl("javascript: window.parqexConfig = window.parqexConfig || { " +
"language : 'de'," +
"email : 'test@example.com'," +
"custom_23793987 : 'test123'," + // Support ID
"custom_23873898 : 'iPad'," + // Device Type (Name)
"darkMode : true," + // Dark mode (boolean)
"some_array : [ 'item1', 'item2' ]};" + // Some array of strings
"window.parqex = window.parqex || {};")
}
}
...
}
Getting data from the webview
To facilitate this, your native app needs to find out from the webview whether this native flow needs to launch after the webview dismisses itself (i.e. if the user took an action that needs to be handled in the native app). Your native app also needs to get the context & information that the user selected/entered/provided at the beginning of the ParqEx flow, so they don't have to re-type their information. Both of these things can be accomplished with the following code.
Define a callback function to receive the user's original request:
private inner class SupportOptionHandler {
@JavascriptInterface
fun handleSupportOption(supportOption: String, userRequest: String) {
// do something with the user request
println("request: $userRequest, option: $requestOption")
}
}
Make this callback available to the webview:
override fun onCreate(savedInstanceState: Bundle?) {
...
my_web_view.addJavascriptInterface(SupportOptionHandler(), HANDLER_NAME)
...
}
companion object {
...
private val HANDLER_NAME = "supportOptionHandler"
}
Make sure you use the exact variable path: window.parqex.native.androidSupportOptionHandler.handle because that is what ParqEx will call when a native app option is clicked by the user in the ParqEx webview.
Clean up: Be sure to clean up the handler as follows:
override fun onDestroy() {
my_web_view.removeJavascriptInterface(HANDLER_NAME)
super.onDestroy()
}
(Optional) Enable webview console logs for debugging If you want to be able to see the webview browser log messages in your Android Studio for debugging purposes, do this:
package com.example.myapplication
import android.content.pm.ApplicationInfo
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlinx.android.synthetic.main.activity_main.*
import android.R.id.message
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView (R.layout.activity_main)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (0 != (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)) {
WebView.setWebContentsDebuggingEnabled(true)
}
}
my_web_view.settings.javaScriptEnabled = true
my_web_view.settings.domStorageEnabled = true
my_web_view.settings.databaseEnabled = true
my_web_view.addJavascriptInterface (SupportOptionHandler(), HANDLER_NAME)
my_web_view.setWebChromeClient(object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
android.util.Log.d("WebView", consoleMessage.message())
return true
}
})
my_web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (url == BASE_URL) {
injectJavaScriptFunction()
}
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
if (url == BASE_URL) {
my_web_view.loadUrl("javascript: window.parqexConfig = window.parqexConfig || { " +
"language : 'de'," +
"email : 'test@example.com'," +
"custom_23793987 : 'test123'," + // Support ID
"custom_23873898 : 'iPad'," + // Device Type (Name)
"darkMode : true," + // Dark mode (boolean)
"some_array : [ 'item1', 'item2' ]};" + // Some array of strings
"window.parqex = window.parqex || {};")
}
}
}
my_web_view.loadUrl(BASE_URL)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Check if the key event was the Back button and if there's history
if (keyCode == KeyEvent.KEYCODE_BACK && my_web_view.canGoBack()) {
my_web_view.goBack()
return true
}
// If it wasn't the Back key or there's no web page history, bubble up to the default
// system behavior (probably exit the activity)
return super.onKeyDown(keyCode, event)
}
override fun onDestroy() {
my_web_view.removeJavascriptInterface(HANDLER_NAME)
super.onDestroy()
}
private fun injectJavaScriptFunction() {
my_web_view.loadUrl(
"javascript: " +
"window.parqex = window.parqex || {};" +
"window.parqex.native = window.parqex.native || {};" +
"window.parqex.native = { androidSupportOptionHandler: {} };" +
"window.parqex.native.androidSupportOptionHandler.handle = " +
"function(option, question) { " +
HANDLER_NAME + ".handleSupportOption(option, question); };"
)
}
private inner class SupportOptionHandler {
@JavascriptInterface
fun handleSupportOption(supportOption: String, userQuestion: String) {
// do something with the user question and support option
println("question: $userQuestion, option: $supportOption")
}
}
companion object {
private val HANDLER_NAME = "supportOptionHandler"
private val BASE_URL = "<YOUR WEB TICKET SUBMISSION URL WITH PARQEX>"
}
}
Closing the webview when the user is done with the ParqEx app
When a user is done using the ParqEx app, they may navigate back to the native app by using the menu, clicking on a back button or the home icon. If they click on any option to end the ParqEx app experience, the webview needs to pass control back to the native app. This happens through another callback function which the ParqEx app/modal calls. Use the following code to handle that callback:
...
my_web_view.addJavascriptInterface (ExitHandler(), EXIT_HANDLER_NAME)
...
private fun injectJavaScriptFunction() {
my_web_view.loadUrl("javascript: window.parqex = window.parqex || {};" +
"window.parqex.native = window.parqex.native || {};" +
"window.parqex.native = { androidExitHandler: {} };" +
"window.parqex.native.androidExitHandler.handle = function() { " +
EXIT_HANDLER_NAME + ".handleExit(); };")
}
private inner class ExitHandler {
@JavascriptInterface
fun handleExit() {
// do something like close the webview
println("EXIT HANDLER CALLED!")
}
}
override fun onDestroy() {
my_web_view.removeJavascriptInterface(EXIT_HANDLER_NAME)
super.onDestroy()
}
companion object {
private val EXIT_HANDLER_NAME = "exitHandler"
}
Injecting multiple JavaScript functions
In order to inject multiple JavaScript functions into the Webview, like handling access options, enter and exit simultaneously, you would set up your injectJavaScriptFunction() like this:
Allowing users to upload photos, documents, and files
To allow the users to upload photos, documents, and files from their local device to the ParqEx app, you will need to add the following code:
import android.net.Uri
import android.content.Intent
import android.webkit.ValueCallback
import android.webkit.WebView
class MainActivity : AppCompatActivity() {
val REQUEST_CODE_LOLIPOP = 1
private val RESULT_CODE_ICE_CREAM = 2
private var mFilePathCallback: ValueCallback<Array<Uri>>? = null
private var mUploadMessage: ValueCallback<Uri>? = null
...
override fun onCreate(savedInstanceState: Bundle?) {
...
my_web_view.setWebChromeClient(object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
android.util.Log.d("WebView", consoleMessage.message())
return true
}
//For Android 5.0 above
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
)
: Boolean {
return this@MainActivity.onShowFileChooser(
webView,
filePathCallback,
fileChooserParams
)
}
// For Android 3.0+
fun openFileChooser(uploadMsg: ValueCallback<Uri>) {
this.openFileChooser(uploadMsg, "*/*")
}
// For Android 3.0+
fun openFileChooser(uploadMsg: ValueCallback<Uri>, acceptType: String) {
this.openFileChooser(uploadMsg, acceptType, null)
}
//For Android 4.1
fun openFileChooser(
uploadMsg: ValueCallback<Uri>,
acceptType: String, capture: String?
) {
this@MainActivity.openFileChooser(uploadMsg, acceptType, capture)
}
})
my_web_view.webViewClient = object : WebViewClient() {
...
}
...
}
...
fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: WebChromeClient.FileChooserParams?
)
: Boolean {
mFilePathCallback?.onReceiveValue(null)
mFilePathCallback = filePathCallback
if (Build.VERSION.SDK_INT >= 21) {
val intent =fileChooserParams?.createIntent()
startActivityForResult(intent, REQUEST_CODE_LOLIPOP)
}
return true
}
fun openFileChooser(
uploadMsg: ValueCallback<Uri>,
acceptType: String, capture: String?
) {
mUploadMessage = uploadMsg
val i = Intent(Intent.ACTION_GET_CONTENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = acceptType
this@MainActivity.startActivityForResult(
Intent.createChooser(i, "File Browser"),
RESULT_CODE_ICE_CREAM
)
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
RESULT_CODE_ICE_CREAM -> {
var uri: Uri? = null
if (data != null) {
uri = data.data
}
mUploadMessage?.onReceiveValue(uri)
mUploadMessage = null
}
REQUEST_CODE_LOLIPOP -> {
if (Build.VERSION.SDK_INT >= 21) {
val results = WebChromeClient.FileChooserParams.parseResult(resultCode, data)
mFilePathCallback?.onReceiveValue(results)
}
mFilePathCallback = null
}
}
}
Intercept URL requests from within a webview
There are many options to intercept URL's within a Webview on Android, each one depends on the scope and the requirements that you have.
Option 1: Override URL loading
Give the host application a chance to take control when a URL is about to be loaded in the current WebView. If a WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the URL. If a WebViewClient is provided, returning true causes the current WebView to abort loading the URL, while returning false causes the WebView to continue loading the URL as usual.
public boolean shouldOverrideUrlLoading (WebView view,
String url)
This option has the next limitations:
It does not catch POST request.
It is not triggered on any resources loaded inside the page. i.e. images, scripts, etc.
It is not triggered on any HTTP request made by JavaScript on the page.
For more information about this method please refer to the documentation
Option 2: Redirect resources loading
Notify the host application that the WebView will load the resource specified by the given url.
webView.webViewClient = object : WebViewClient() {
override fun onLoadResource(view: WebView, url: String) {
view.stopLoading() view.loadUrl(newUrl) // this will trigger onLoadResource }}
onLoadResource providers similar functionality to shouldOverrideUrlLoading. ButonLoadResource will be called for any resources (images, scripts, etc) loaded on the current page including the page itself.
You must put an exit condition on the handling logic since this function will be triggered on loadUrl(newUrl). For example:
webView.webViewClient = object : WebViewClient() {
override fun onLoadResource(view: WebView, url: String) {
// exit the redirect loop if landed on homepage if(url.endsWith("home.html")) return // redirect to home page if the page to load is error page if(url.endsWith("error.html")) { view.stopLoading() view.loadUrl("https//host.com/home.html") } }}
This option has the next limitations:
It is not triggered on any HTTP request made by JavaScript on the page.
For more information please refer to the documentation.
Option 3: Handle all requests
Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used.
This callback is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs.
In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.
This function is running in a background thread similar to how you execute an API call in the background thread. Any attempt to modify the content of the WebView inside this function will cause an exception. i.e. loadUrl, evaluateJavascript, etc.
For API<21 please use:
public WebResourceResponse shouldInterceptRequest (WebView view, String url)
This option has the next limitations:
There is no payload field on the WebResourceRequest. For example, if you want to create a new user with a POST API request. You cannot get the POST payload from the WebResourceRequest. - This method is called on a thread other than the UI thread so clients should exercise caution when accessing private data or the view system.
For more information please refer to the documentation
Other options: There are other non-conventional options that can allow us to:
Resolve payload for POST requests. - Ensure JS override available on every page. - Inject JS code into each HTML page.
These options are for more specifics requirements and are using JS or HTML overriding if you want to explore those options, please refer to this post, but most of the scenarios are cover on the above options.