Http://medium.com/madmuc/int…
Original author: medium.com/@madmuc
Published: October 29, 2019 -5 minutes to read
The problem
I have a requirement that all HTTP requests from webViews on Android be handled locally. For example, provide assets for HTML rendering and handle API requests without Internet connection. I also have no control over what HTML content is loaded into the WebView, and instead use a single URL as a starting point.
Version 0.5 — overrides URL loading
For example, require that the home page be redirected whenever an error page is about to load. I can use the following code.
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}}}Copy the code
As the name implies, shouldOverrideUrlLoading returns whether URL loading should be overridden. If the function returns true, the WebView will abort the loading of requests passed into the function.
The problem
- It does not capture POST requests.
- It is not triggered on any resources loaded within the page, such as images, scripts, etc.
- It will not be triggered by any HTTP request made by JavaScript on the page.
Version 1.0 – Redirects resource loads
webView.webViewClient = object : WebViewClient() {
override fun onLoadResource(view: WebView, url: String) {
view.stopLoading()
view.loadUrl(newUrl) // this will trigger onLoadResource}}Copy the code
OnLoadResource provides similar functionality to shouldOverrideUrlLoading. ButonLoadResource will call any resources (images, scripts, etc.) loaded on the current page, including the page itself.
You must set an exit condition on the processing logic because 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")}}}Copy the code
The problem
- It will not be triggered in any HTTP request made by JavaScript on the page.
Version 1.5 – Handles all requests
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return super.shouldInterceptRequest(view, request)
}
}
Copy the code
This is a very powerful callback that allows you to provide a complete response to any request on the current page, including data: and File: modes. This captures requests made by JavaScript on the page.
This function runs in a background thread, similar to how you perform API calls in a background thread. Any attempt to modify the WebView contents in this function will raise an exception, such as loadUrl, evaluationJavascript, etc.
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)
}
}
}
Copy the code
As another example, we want to provide a user API response from the local DB.
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return if (request.url.path == "api/user") {
val userId = request.url.getQueryParameter("userId") repository.getUser(userId)? .let { WebResourceResponse("application/json"."utf-8", ByteArrayInputStream(it.toJson().toByteArray()) ) } ? : WebResourceResponse("application/json"."utf-8".404."User not found",
emptyMap(),
EmptyInputStream()
)
} else {
super.shouldInterceptRequest(view, request)
}
}
}
Copy the code
The problem
- The WebResourceRequest does not have a payload field. For example, if you want to create a new user using the POST API request. You cannot get the POST payload from WebResourceRequest.
Version 2.0 — Parses the payload of a POST request.
StackOverflow has a few thoughts on this. One is to override the XMLHttpRequest interface in JavaScript. The basic idea is to override xmlHttprequet.send to record sending the payload and retrieve the payload on shouldInterceptRequest. There are three parts to this solution.
Part ONE –JavaScript rewriting
XMLHttpRequest.prototype.origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
// these will be the key to retrieve the payload
this.recordedMethod = method;
this.recordedUrl = url;
this.origOpen(method, url, async, user, password);
};
XMLHttpRequest.prototype.origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
// interceptor is a Kotlin interface added in WebView
if(body) recorder.recordPayload(this.recordedMethod, this.recordedUrl, body);
this.origSend(body);
};
Copy the code
This code snippet overwrites the xmlHttprequest. open and xmlHttprequest. send functions, passing the HTTP method, URL, and HTTP payload to a Kotlin function. Save the code snippet to a JS file in the Assets folder and load it into the WebView using the following methods.
webView.evaluateJavascript(
assets.open("override.js").reader().readText(),
null
)
Copy the code
Part 2 – Kotlin class using @javascriptInterface
class PayloadRecorder {
private val payloadMap: MutableMap<String, String> =
mutableMapOf()
@JavascriptInterface
fun recordPayload(
method: String,
url: String,
payload: String
) {
payloadMap["$method-$url"] = payload
}
fun getPayload(
method: String,
url: String
): String? =
payloadMap["$method-$url"]}Copy the code
This class will receive the recordPayload call from the previous JS code and place the payload into a map. We can add an instance of this class to the WebView using
val recorder = PayloadRecorder()
webView.addJavascriptInterface(recorder, "recorder")
Copy the code
Part 3 – Retrieving the POST payload
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val payload = recorder.getPayload(request.method, request.url.toString())
// handle the request with the given payload and return the response
return super.shouldInterceptRequest(view, request)
}
}
Copy the code
This section is very similar to the 1.5 method, except that we can get the recoded POST payload from the Kotlin JavaScript class.
The problem
- For Android API 24+, the state generated by evaluateJavascript is not persisted across pages. This means that any new pages loaded will not have the JavaScript overlay of the first part.
Version 2.1 – Ensure that JS overlays are available on every page
webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(
view: WebView,
url: String,
favicon: Bitmap?). {
webView.evaluateJavascript(
assets.open("override.js").reader().readText(),
null)}override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val payload = recorder.getPayload(request.method, request.url.toString())
// handle the request with the given payload and return the response
return super.shouldInterceptRequest(view, request)
}
}
Copy the code
The WebViewClient provides onPageStarted, which is called every time a page starts loading onto the WebView. We will execute the JS override code Snipper every time the page starts.
The problem
- OnPageStarted is not called when a page is loaded into the iFrame of the current page.
Version 2.2 – Inject JS code into every HTML page.
The only function that almost every type of request calls is shouldInterceptRequest. However, we are not allowed to execute any JS code inside this function because it runs in a background thread. My solution is to inject JS code into the HTML content in the WebResourceResponse returned by shouldInterceptRequest.
For example, you can add JS overrides directly to your HTML.
<html>
<head>
<script type="text/javascript" src="file:///android_asset/override.js" >
</script>
</head>
</html>
Copy the code
You can also inject JS code dynamically into HTML pages.
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val resp = getResponse(request)
if(resp.mMimeType == "text/html")
injectJs(resp)
return resp
}
}
Copy the code
But remember
- Inject JS code at the top of the HTML to make it work as quickly as possible.
- Do not parse the entire HTML content, as this is inefficient and the HTML may be invalid.
You could probably do a text search on and append a script line to it.
You can probably do a text search on the top and then append a script to it. If you enjoyed reading this article, please clap your hands using the 👏 button and share it through your circles. Thank you.
Translation via www.DeepL.com/Translator (free version)