Android WebView has a function called * addJavascriptInterface * that embeds Java objects in WebView. This makes it possible to call the method defined in the Java object from the Javascript embedded in the Web page displayed in the WebView.
This time, I briefly investigated how the callback method of WebView is called when OkHttp is used for HTTP communication in WebView. If you are tired of reading, you may read only the final "Summary".
The source code of Activity that uses WebView looks like this. It's cute that the sample code is poor.
MainActivity.kt
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "WebViewTest_"
const val TAG_OKHTTP = "OkHttp_WebViewTest"
const val TAG_TID = "Tid_webView"
const val JS_INTERFACE_NAME = "Android"
const val INITIAL_ENDPOINT = "http://${sever_ipAddress}:10000/"
}
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
initWebView()
}
@SuppressLint("JavascriptInterface", "SetJavaScriptEnabled")
fun initWebView() {
Log.d(TAG_TID, "MainActivity#initWebView called. tid = " + Thread.currentThread().id)
binding.webView.apply {
settings.javaScriptEnabled = true
addJavascriptInterface(this@MainActivity, JS_INTERFACE_NAME)
webViewClient = object: WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
Log.d(TAG, "shouldOverrideUrlLoading called. url = " + request?.url.toString())
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
Log.d(TAG, "onPageStarted called. url = " + url!!)
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView?, url: String?) {
Log.d(TAG, "onPageFinished called. url = " + url!!)
super.onPageFinished(view, url)
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
Log.d(TAG, "onReceivedError error = " + error!!)
super.onReceivedError(view, request, error)
}
override fun onLoadResource(view: WebView?, url: String?) {
Log.d(TAG, "onLoadResource called. url = " + url!!)
super.onLoadResource(view, url)
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
Log.d(TAG_TID, "shouldInterceptRequest called. tid = " + Thread.currentThread().id)
if (!request?.url.toString().endsWith("/favicon.ico")) {
Log.d(TAG, "shouldInterceptRequest. url = " + request?.url.toString())
}
val latch = CountDownLatch(1)
var res: InputStream? = null
val call = if (request!!.url.path!!.endsWith("/getJsBySrc") or
request.url.path!!.endsWith("/doHttpReqFromJsCode.js")
) {
createOkHttpClient().newCall(Request.Builder().url(request.url.toString()).method("POST", RequestBody.create(null, "hoge")).build())
} else {
createOkHttpClient().newCall(Request.Builder().url(request.url.toString()).build())
}
call.enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
//Log.d(TAG_OKHTTP, "okhttp Callback#onFailure called. callUrl = " + call.request().url())
Log.d(TAG_OKHTTP, "okhttp Callback#onFailure called. error = " + e.message.toString())
latch.countDown()
}
override fun onResponse(call: Call, response: Response) {
//Log.d(TAG_OKHTTP, "okhttp Callback#onResponse called. callUrl = " + call.request().url())
Log.d(TAG_OKHTTP, "okhttp Callback#onResponse called. resUrl = " + response.request().url())
res = response.body()?.byteStream()
latch.countDown()
}
})
latch.await()
return WebResourceResponse(
"text/html", "UTF-8",
res
)
}
}
loadUrl(INITIAL_ENDPOINT)
}
}
private val cookieStore = HashMap<String, MutableList<Cookie>>()
fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
Log.d(TAG_TID, "okhttp intercepted: tid = " + Thread.currentThread().id)
Log.d(TAG_OKHTTP, "okhttp intercepted: " + chain.request().url().toString())
chain.proceed(chain.request())
}
//.connectTimeout(1, TimeUnit.MILLISECONDS)
.cookieJar(object: CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
cookieStore[url.host()] = cookies
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
val cookies = cookieStore[url.host()]
return cookies ?: ArrayList()
}
})
.build()
}
@JavascriptInterface
fun showToast(str: String) {
Toast.makeText(this, str, Toast.LENGTH_LONG).show()
}
}
The points are as follows.
--Added MainActivity as Javascript Interface with the name "Android" --Define a method called showToast so that it can be executed by Javascript from within WebView --Log is output when each callback is called, and you can check how HTTP communication is controlled.
The implementation on the server side is as follows. This time it is assumed that the client will access the root endpoint.
app/Main.hs
1 {-# LANGUAGE OverloadedStrings #-}
2 module Main where
3
4 import Network.Wai.Middleware.Static
5 import Network.HTTP.Types.Status
6 import Web.Spock
7 import Web.Spock.Config
8
9 import Control.Monad.Trans
10 import qualified Data.Text as T
11
12 data MySession = MySession { msUserId :: Maybe String }
13
14 main :: IO ()
15 main = do
16 spockCfg <- defaultSpockCfg (MySession Nothing) PCNoDatabase ()
17 runSpock 10000 (spock spockCfg app)
18
19 app :: SpockM () MySession () ()
20 app = do
21 middleware $ staticPolicy (addBase "static")
22 get root $
23 (modifySession $ \sess -> sess { msUserId = Just "dummy" }) >> redirect "http://${server_ipAddress}:10000/submitFormByJs"
24 get ("submitFormByJs") $ do
25 sess <- readSession
26 case msUserId sess of
27 Nothing -> setStatus status401 >> text "No session"
28 Just _ -> (liftIO . readFile $ "static/submitFormByJs.html") >>= html . T.pack
29 post ("getJsBySrc") $
30 (liftIO . readFile $ "static/getJsBySrc.html") >>= html . T.pack
31 post ("doHttpReqFromJsCode.js") $
32 file "application/javascript" "static/doHttpReqFromJsCode.js"
33 get ("fromXMLHttpReq") $
34 (liftIO . readFile $ "static/fromXMLHttpReq.html") >>= html . T.pack
35 get ("onReceivedXHRResAndSetLocationHref") $
36 (liftIO . readFile $ "static/onReceivedXHRResAndSetLocationHref.html") >>= html . T.pack
37 get ("favicon.ico") $
38 file "image/png" "favicon.ico"
~
The following is a summary of the communication flow when accessing the root endpoint of the above Web server.
root
"submitFormByJs"
"getJsBySrc"
"doHttpReqFromJsCode.js"
"fromXMLHttpReq"
"onReceivedXHRResAndSetLocationHref"
root Redirect to "submitFormByJs" with HTTP status 302
"submitFormByJs" The \ <form > tag responds to html that sends a POST request to the "getJsBySrc" endpoint. The above POST request is executed when WebView interprets this HTML.
submitFormByJs.html
1 <html>
2 <body>
3 <script type="text/javascript">
4 const ua = window.navigator.userAgent
5 if (ua.includes("Android")) {
6 Android.showToast("A post request is going to be sent from a <form> tag.");
7 }
8 </script>
9 <script type="text/javascript">
10 function doPost() {
11 document.form1.method = "post";
12 document.form1.submit();
13 }
14 </script>
15 <form name="form1" action="http://${server_ip}:10000/getJsBySrc" method="post">
16 </form>
17 <h1>submitFormByJs</h1>
18 <script type="text/javascript">
19 doPost();
20 </script>
21
22 </body>
23 </html>
~
getJsBySrc.html
1 <html>
2 <body>
3 <h1>GetJSBySrc</h1>
4 <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> -->
5 <script type="text/javascript">
6 const ua = window.navigator.userAgent
7 if (ua.includes("Android")) {
8 Android.showToast("A javascript file is going to be loaded by <script ... src=...>");
9 }
10 </script>
11 <script type="text/javascript" src="http://${server_ip}:10000/doHttpReqFromJsCode.js"></script>
12 </body>
13 </html>
doHttpReqFromJsCode.js
1 const xhr = new XMLHttpRequest();
2
3 xhr.open('POST', 'http://${server_ip}:10000/fromXMLHttpReq.html');
4 xhr.send();
5
6 xhr.onreadystatechange = function() {
7 if(xhr.readyState === 4 && xhr.status === 200) {
8 const ua = window.navigator.userAgent
9 if (ua.includes("Android")) {
10 Android.showToast("doHttpReqFromJsCode.js");
11 }
12 location.href="http://${server_ip}:10000/onReceivedXHRResAndSetLocationHref"
13 }
14 }
fromXMLHttpReq.html
1 <html>
2 <body>
3 <h1>XMLHttpRequest</h1>
4 <script type="text/javascript">
5 const ua = window.navigator.userAgent
6 if (ua.includes("Android")) {
7 Android.showToast("XMLHttpRequest succeeded. location.href is going to be modified.");
8 }
9 </script>
10 </body>
11 </html>
onReceivedXHRResAndSetLocationHref.html
1 <html>
2 <body>
3 <script type="text/javascript">
4 const ua = window.navigator.userAgent
5 if (ua.includes("Android")) {
6 Android.showToast("location.href has been modified.");
7 }
8 </script>
9 <h1>Received XMLHttpRequest's response</h1>
10 </body>
11 </html>
The log output when loadUrl ("http: // $ {server_ip}: 10000 /") is done in WebView is as follows.
Tag "WebViewTest_Filter with
1 D/WebViewTest_: shouldInterceptRequest. url = http://192.168.100.151:10000/
2 D/WebViewTest_: onPageStarted called. url = http://192.168.100.151:10000/
3 D/WebViewTest_: JavascriptInterface method called. str = A post request is going to be sent from a <form> tag.
4 D/WebViewTest_: shouldInterceptRequest. url = http://192.168.100.151:10000/getJsBySrc
5 D/WebViewTest_: onPageFinished called. url = http://192.168.100.151:10000/
6 D/WebViewTest_: onPageStarted called. url = http://192.168.100.151:10000/getJsBySrc
7 D/WebViewTest_: shouldInterceptRequest. url = http://192.168.100.151:10000/doHttpReqFromJsCode.js
8 D/WebViewTest_: JavascriptInterface method called. str = A javascript file is going to be loaded by <script ... src=...>
9 D/WebViewTest_: shouldInterceptRequest. url = http://192.168.100.151:10000/fromXMLHttpReq.html
10 D/WebViewTest_: onPageFinished called. url = http://192.168.100.151:10000/getJsBySrc
11 D/WebViewTest_: JavascriptInterface method called. str = doHttpReqFromJsCode.js
12 D/WebViewTest_: shouldOverrideUrlLoading called. url = http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
13 D/WebViewTest_: onPageStarted called. url = http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
14 D/WebViewTest_: shouldInterceptRequest. url = http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
15 D/WebViewTest_: JavascriptInterface method called. str = location.href has been modified.
16 D/WebViewTest_: onPageFinished called. url = http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
The Log output result of the OkHttp communication part is as follows.
Tag "OkHttp_Filter with "WebViewTest"
18 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/
19 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/submitFormByJs
20 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/submitFormByJs
21 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/getJsBySrc
22 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/favicon.ico
23 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/getJsBySrc
24 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/favicon.ico
25 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/doHttpReqFromJsCode.js
26 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/doHttpReqFromJsCode.js
27 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/fromXMLHttpReq.html
28 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/fromXMLHttpReq.html
29 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/favicon.ico
30 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/favicon.ico
31 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
32 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/onReceivedXHRResAndSetLocationHref
33 D/OkHttp_WebViewTest: okhttp intercepted: http://192.168.100.151:10000/favicon.ico
34 D/OkHttp_WebViewTest: okhttp Callback#onResponse called. resUrl = http://192.168.100.151:10000/favicon.ico
Let's summarize the above results. (Because of the result of verification in my local environment, there may be a difference in behavior.)
--HTTP communication that occurs in the \ <form > tag in HTML --HTTP communication that occurs in \
Recommended Posts