The Android WebView is great for presenting users with web content in native or hybrid applications. Its ability to bind JavaScript code to a client-side interface, navigation controls, and relatively small API surface are all great. However, this week I ran into an unfortunate omission. There is no proper mechanism for inspecting response headers on WebView requests.
In our particular case, we needed some information from faithlife.com that is returned as a custom header in order to handle a specific edge-case in our app well. After some exploration, it seemed our best bet was to make the HTTP request ourselves without the help of WebView so we could inspect the headers. We use OkHttp for networking, but the idea is the same so long as you’re able to inspect response headers using your networking client of choice. That looks something like this:
val okHttpClient = OkHttpClient.Builder().Build()
val request = Request.Builder()
.url(requestUrl)
.build()
val response = okHttpClient.newCall(request).execute()
val importantInfo = response.headers.firstOrNull { it.key == "X-Important-Info" }
importantInfo?.let {
// We have our information.
}
Note: When using this type of logic in
shouldOverrideUrlLoading(WebView, WebViewRequest)
, be sure to copy the request headers on the second parameter into the headers of the initial get request (part of the RequestBuilder if you use OkHttp).
Since this WebView is showing user content on faithlife.com and is running within the context of our app, we’ll need to make sure faithlife.com knows that we’re authenticated. The site handles authentication via cookies. That cookie information is passed back from the initial request via Set-Cookie
headers. Fortunately, it’s pretty easy to get that data where it needs to go.
response.headers("Set-Cookie")?.forEach { setCookieHeader ->
CookieManager.getInstance()
.setCookie(requestUrl, setCookieHeader)
}
Now we have access to the response headers and we’ve made sure our cookies are up to date, but we still have to show the web page. Since we already have the HTML of the page from the initial GET, we can avoid loading the webpage again by loading that HTML into the WebView directly. The thing to pay attention to here is that the first parameter to loadDataWithBaseURL
is used to resolve relative paths and for applying JavaScript’s same origin policy. You want to be sure to use the response’s last known url when loading this data in case the first call to the requestUrl triggers a redirect. response.request().url()
is the okhttp3.Response
way of getting the last url in the redirect chain.
if (response.isSuccessful) {
val responseBody = response.body()
responseBody?.let {
webView.loadDataWithBaseURL(
response.request().url().toString(),
responseBody,
response.header("Content-Type") ?: "text/html",
null,
requestUrl
)
}
}
Now the WebView takes back over as it renders the HTML, loading other resources just as if it had handled the initial get request also.
We like Kotlin a lot. As it currently stands, the upcoming version of the app is exclusively written in the language. If you’re an Android developer and find problems like this fun, we’re hiring!
We have some exciting things planned for the Faithlife App [play store link]. Stay tuned!
Thanks to Dustin Masters & Logan Ash for reviewing earlier versions of the post.
Posted by Justin Brooks on July 31, 2018