I took a quick look at how WebView's Javascript Interface is called when using OkHttp.

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".

Implementation of Activity using WebView

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.

Server-side implementation

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"
~                                                    

Communication flow summary

The following is a summary of the communication flow when accessing the root endpoint of the above Web server.

  1. root

  2. "submitFormByJs"

  3. "getJsBySrc"

  4. "doHttpReqFromJsCode.js"

  5. "fromXMLHttpReq"

  6. "onReceivedXHRResAndSetLocationHref"

  7. root Redirect to "submitFormByJs" with HTTP status 302

  8. "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>
~               
  1. "getJsBySrc" By executing the \ <script > tag, html that makes a GET request to the endpoint "doHttpReqFromJsCode" is returned.

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>                                                                                                                                   
  1. "doHttpReqFromJsCode.js" XMLHttpReqeust responds Javascript that performs HTTP communication to the endpoint "fromXMLHttpReq". When the above communication is successful, redirect to the endpoint "onReceivedXHRResAndSetLocationHref" by GET communication by location.href.

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 }
  1. "fromXMLHttpReq" Accessed by executing XMLHttpRequest in doHttpReqFromJsCode.js.

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>
  1. ""onReceivedXHRResAndSetLocationHref" Accessed when XMLHttpRequest communication by doHttpReqFromJsCode.js is successful.

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>

Log output result

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

Summary

Let's summarize the above results. (Because of the result of verification in my local environment, there may be a difference in behavior.)

WebView shouldInterceptRequest is called

--HTTP communication that occurs in the \ <form > tag in HTML --HTTP communication that occurs in \

Recommended Posts

I took a quick look at how WebView's Javascript Interface is called when using OkHttp.
I took a look at the resources of Azure Container Instance
A quick look at the Monty Hall problem
I want to issue a connection when a database is created using Spring and MyBatis
I took a second look at Tribuo published by Oracle. Tribuo --A Java prediction library (v4.0)