Android Implementation Guide

  1. In Android Studio, go to your manifests/AndroidManifest.xml file and add the following:

<uses-permission android:name="android.permission.INTERNET"/>

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:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  1. In your layout/activity_main.xml file add the following:

<WebView
   android:id="@+id/my_web_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   />
  1. In your MainActivity.kt file add the following:

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.

  1. 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")
  }
}
  1. 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"
  }
  1. Inject the handler onto the webpage:

  override fun onCreate(savedInstanceState: Bundle?) {
    ...
    my_web_view.webViewClient = object : WebViewClient() {
      override fun onPageFinished(view: WebView, url: String) {
        if (url == BASE_URL) {
          injectJavaScriptFunction()
        }
      }
    }
    ...
  }

  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); };"
    )
  }

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.

  1. Clean up: Be sure to clean up the handler as follows:

  override fun onDestroy() {
    my_web_view.removeJavascriptInterface(HANDLER_NAME)
    super.onDestroy()
  }
  1. (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:

  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
      }
    })
    ...
  }
  1. Now your MainActivity.kt should look like 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:

private fun injectJavaScriptFunction() {
    my_web_view.loadUrl(
        "javascript: " +
            "window.parqex = window.parqex || {};" +
            "window.parqex.native = window.parqex.native || {};" +
            "window.parqex.native = { androidSupportOptionHandler: {}, androidExitHandler: {} };" +
            "window.parqex.native.androidSupportOptionHandler.handle = " +
            "function(option, question) { " +
            HANDLER_NAME + ".handleSupportOption(option, question); };" +
            "window.parqex.native.androidExitHandler.handle = function() { " +
            EXIT_HANDLER_NAME + ".handleExit(); };"
    )
}

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.

webView.webViewClient = object : WebViewClient() {  
 override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest ): Boolean { return if(request.url.lastPathSegment == "error.html") { view.loadUrl("https//host.com/home.html") true } else { false } }}  

For API<24 please use

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.

webView.webViewClient = object : WebViewClient() {  
 override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { return super.shouldInterceptRequest(view, request) }}  

For example, we want to provide a local error page.

webView.webViewClient = object : WebViewClient() {  
 override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { return if (request.url.lastPathSegment == "error.html") { WebResourceResponse( "text/html", "utf-8", assets.open("error") ) } else { super.shouldInterceptRequest(view, request) } }}  

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.

Last updated