Skip to main content

Android 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 internet within the app.

If you allow your customers to submit attachments through your Solvvy 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.webViewClient = object : WebViewClient() {
}
my_web_view.loadUrl(BASE_URL)
}

companion object {
private val BASE_URL = "<YOUR WEBVIEW URL WITH SOLVVY>"
}
}

This is the basic code for opening your webview URL (which should auto-launch Solvvy). If Solvvy is not installed on your webview URL or does not auto-launch, contact your Solvvy Sales Engineer or Solutions Engineer. Note: to customize the behavior of the webview in various ways, please consult the documentation.

Navigating back to Solvvy after opening a link to a KB article

Users have the ability to see the original KB articles that contain the solutions Solvvy 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 Solvvy. 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 Solvvy running in the webview, e.g. to specify which language the app is using so Solvvy can be properly localized, or helpful metadata that needs to be included on the ticket if the user does not self-serve (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.solvvyConfig object. If you want the data to be passed directly into tickets 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.solvvyConfig = window.solvvyConfig || { " +
"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.solvvy = window.solvvy || {};")
}
}
...
}

Getting data from the webview

When a user is not able to self-serve, Solvvy presents a list of options (or channels) for contacting support (or automatically defaults to one if only one is configured). Most of these support options can be handled, or executed, within the Solvvy flow, such as email ticket submission. However, for some support options (e.g. live chat), it may be preferable to execute the support contact flow directly from the native app (e.g. using a 3rd party native SDK). To facilitate this, your native app needs to find out from the webview whether this native support flow needs to launch after the webview dismisses itself (i.e. if the user was not able to self-serve). Your native app also needs to get the question that the user typed in at the beginning of the Solvvy flow, so they don't have to re-type their issue. Both of these things can be accomplished with the following code.

  1. Define a callback function to receive the user's original 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")
}
}
  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.solvvy = window.solvvy || {};" +
"window.solvvy.native = window.solvvy.native || {};" +
"window.solvvy.native = { androidSupportOptionHandler: {} };" +
"window.solvvy.native.androidSupportOptionHandler.handle = " +
"function(option, question) { " +
HANDLER_NAME + ".handleSupportOption(option, question); };"
)
}

Make sure you use the exact variable path: window.solvvy.native.androidSupportOptionHandler.handle because that is what Solvvy will call when a support option is clicked by the user.

  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.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.solvvyConfig = window.solvvyConfig || { " +
"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.solvvy = window.solvvy || {};")
}
}
}
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.solvvy = window.solvvy || {};" +
"window.solvvy.native = window.solvvy.native || {};" +
"window.solvvy.native = { androidSupportOptionHandler: {} };" +
"window.solvvy.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 WEBVIEW URL WITH SOLVVY>"
}
}

Closing the webview when the user self-served

When a user is satisfied with one of the answers returned, they can click "Yes" to indicate they got their answer. On the next screen, if they click "Close" to end the Solvvy experience, the webview needs to pass control back to the native app. This happens through another callback function which the Solvvy modal calls when that "Close" button is clicked. 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.solvvy = window.solvvy || {};" +
"window.solvvy.native = window.solvvy.native || {};" +
"window.solvvy.native = { androidExitHandler: {} };" +
"window.solvvy.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 support options and exit simlutaneously, you would set up your injectJavaScriptFunction() like this:

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

Allowing attachments on tickets

If you want to allow Email/Ticket as one of the options for contacting support, and you want to allow the users to add attachments from their local device to the ticket form, then 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 informatioon 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. But onLoadResource 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 no 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.

Load error handling

Possible scenarios where you might want to handle load errors include:

  • Network issues
  • Incompatible browser or device*
  • Solvvy load failure

Although these scenarios are rare, the following load error handler can be implemented to provide a cleaner experience for your users. The possible load errors are:

  • "loading_timeout" - Solvvy did not load within 10 seconds
  • "loading_failed" - Solvvy failed to load
  • "incompatible_browser" - Incompatible browser or device*

If this handler is not implemented, where possible, the Solvvy team will implement a generic fallback to redirect to your web contact form.

  1. Define a callback function to receive the user's original question:
private inner class LoadErrorHandler {
@JavascriptInterface
fun handleLoadError(error: String) {
// do something with the error
println("error: $error")
}
}
  1. Make this callback available to the webview:
  override fun onCreate(savedInstanceState: Bundle?) {
...
my_web_view.addJavascriptInterface(LoadErrorHandler(), HANDLER_NAME)
...
}

companion object {
...
private val HANDLER_NAME = "loadErrorHandler"
}
  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.solvvy = window.solvvy || {};" +
"window.solvvy.native = window.solvvy.native || {};" +
"window.solvvy.native = { androidLoadErrorHandler: {} };" +
"window.solvvy.native.androidLoadErrorHandler.handle = " +
"function(error) { " +
HANDLER_NAME + ".handleLoadError(error); };"
)
}

Make sure you use the exact variable path: window.solvvy.native.androidLoadErrorHandler.handle because that is what Solvvy will call when a load error occurs.

  1. Clean up

Be sure to clean up the handler as follows:

  override fun onDestroy() {
my_web_view.removeJavascriptInterface(HANDLER_NAME)
super.onDestroy()
}