Extracting HTML properties from an Android Web View in Jetpack Compose

Stelios Papamichail
4 min readJun 13, 2022

--

You can now find this and other posts over on my substack page at https://thatgreekengineer.substack.com/ !

I was recently tasked with the implementation of a 3DS payment verification flow for one of our new projects, and I had to pass various HTML properties to our backend in order to initialize the process. These properties include things such as Navigator.language, Navigator.javaEnabled, Screen.ColorDepth and many more. This begs the question, how can we retrieve such HTML properties through an Android WebView dynamically? Well, the answer is… JavaScript interfaces. Let’s see how to do that in a Jetpack Compose screen.

1. Adding the WebView

First and foremost, let’s start by creating a separate @Composable for our WebView screen.

The only thing that we need to do, is to use the AndroidView composable which will compose an Android view from the given factory method, which in this case will be the WebView. That’s pretty much it! I’ve also added a simple BackHandler snippet to help you handle back navigation while inside the WebView.

@Composable
fun Payments3DSWebViewScreen(
navController: NavController
) {
val context = LocalContext.current
var webView by remember { mutableStateOf<WebView?>(null)

Column(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = {
WebView(context).apply {
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
}
}
,
modifier = Modifier
.fillMaxSize()
.background(Color.White)
)
}
BackHandler {
if (webView != null) {
if (webView!!.canGoBack()) {
webView?.goBack()
} else {
navController.popBackStack()
}
} else {
navController.popBackStack()
}
}
}

2. Enabling JavaScript in our WebView

This is very simple, and it only requires that you add the settings.javaScriptEnabled = true line inside the WebViewblock like so:

WebView(context).apply {
settings.javaScriptEnabled = true
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
}

That’s it, now that JavaScript has been enabled, we can start interfacing with it.

Quick reminder: You should strongly consider if you need to enable JavaScript in your application since it can lead to security issues. So do your research and act according to your project’s needs.

3. JavaScript interfaces in Android

So what’s a JS interface? It’s basically a function within your android app that can be called inside the Web View as a JS script. This means that since it will be run inside a JS script, you can access most properties that would be accessible by such a script in a browser. These functions must be annotated with @JavascriptInterface! Here’s an example of such an interface method from my app:

class PaymentsJSInterface() {
lateinit var language: String
var javaEnabled: Boolean = false
lateinit var
screenColorDepth: String
lateinit var screenHeight: String
lateinit var screenWidth: String
lateinit var timezoneOffset: String
lateinit var userAgent: String

@JavascriptInterface
fun getNavigatorInfo(
lang: String,
javaEnabled: Boolean,
screenColorDepth: String,
screenHeight: String,
screenWidth: String,
tzOffset: String,
userAgent: String
) {
language = lang
this.javaEnabled = javaEnabled
this.screenColorDepth = screenColorDepth
this.screenHeight = screenHeight
this.screenWidth = screenWidth
timezoneOffset = tzOffset
this.userAgent = userAgent
Log.d(
"PAYMENTJS",
"$lang | $javaEnabled | $screenColorDepth | $screenHeight | $screenWidth | $tzOffset | $userAgent"
)
}
}

As you can see, I’ve added the function inside a separate class, which will be needed in the next steps. You will also notice that it accepts a few parameters that will be set from our custom JS code that will be run inside the Web View.

4. Using our JS interface within the web view

Alright, so now that we have our interface method set up, let’s connect it with our Web View. This is achieved in two steps:

First, we need to call theaddJavascriptInterface(paymentsJSInterface, "APP") inside our WebView block, which, as you can tell, requires an instance of the class containing the method that we previously annotated with the @JavascriptInterface and a “namespace” that will be used to specify which function from that namespace (namely, our app), JS should run. This can be any string that you like.

The second step is adding a call to the loadUrl() method and specifying that instead of a URL, we want it to load a custom JS script that will call a JavaScriptInterface method from the namespace that we set earlier. This call should also be placed inside the WebView block. Here’s a complete example:

val paymentsJSInterface by remember { mutableStateOf(PaymentsJSInterface()) }
AndroidView(
factory = {
WebView(context).apply {
settings.javaScriptEnabled = true
addJavascriptInterface(paymentsJSInterface, "APP")
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
webView = this
webView?.loadUrl("javascript: APP.getNavigatorInfo(navigator.language || navigator.userLanguage, navigator.javaEnabled(), screen.colorDepth, screen.height, screen.width, new Date().getTimezoneOffset(),navigator.userAgent);")
}
}
,
modifier = Modifier
.fillMaxSize()
.background(Color.White)
)

As you can see here, I’ve set my app’s namespace to “APP” for this example. Now, inside the loadUrl method, we specify that this is a custom JS script by beginning the “URL” with javascript: and then calling our getNavigatorInfo() method through our APP namespace.

You will also notice that we now have full access to any JS-related object inside this custom JS script. So for this method, I can simply access the browser’s language through navigator.language or the user’s screen color depth through screen.colorDepth.

5. Seeing everything in action

Alright, now that everything is ready, let’s run the app! Once the WebView screen comes to the foreground, the loadUrl method will execute our custom JS script, thus calling the getNavigatorInfo() method, which should then log a few of the necessary JS/HTML properties.

Aaaand….

It ain’t much, but it’s honest work!

6. La fin

You’ve made it, congratulations! Now you can use this knowledge and do even greater things, such as passing these fields to your ViewModel for further analysis or redirecting/authenticating users based on the data!

Celebrate In Love GIF By HBO Max

--

--

Stelios Papamichail

Android developer, Comp. Science student & fitness enthusiast