Search code examples
androidkotlinwebviewretrofit2kotlin-coroutines

Android webView - Intercept all request - Url filtering based on api response


I'm trying to intercept webview requests and filter them based on an API response. I'm using a viewModel who has a public onFilter method that calls the API via a repository:

private val _filterResult = MutableLiveData<WebFilterModel>()
val filterResult: LiveData<WebFilterModel> = _filterResult
fun onFilter(url: String) {
    viewModelScope.launch(Dispatchers.Main) {
        val result = filterUseCase.invoke(url)
        if (result.isSuccessful) {
            Log.d("filter", "result onFilter: " + result.body()!!.result)
            _filterResult.value = result.body()
        }
    }
}

The viewModel has a variable of type LiveData which is observed in the private method for my custom webView client, this method called filterRequest is used in the web client's overridden method, shouldInterceptRequest. At time, the shouldOverrideLoading method returns false:

@OptIn(DelicateCoroutinesApi::class)
private fun filterRequest(view: WebView, request: WebResourceRequest?) {
    GlobalScope.launch(Dispatchers.Main) {
        viewModel.onFilter(request!!.url.toString())
        viewModel.filterResult.observe(activity, Observer{
            if (it.result == "ok") {
                shouldBlock = false
                view.loadUrl(it.loadFilterUrl)
                Log.d("filter", "filterRequest ok")
            } else {
                view.post(Runnable {
                    view.stopLoading()
                })

                val url = "https://www.google.com"
                shouldBlock = true
                view.post(Runnable {
                    view.loadUrl(url)
                })
                Log.d("filter", "filterRequest no")
            }
        })
    }
}

override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {

    return false
}

override fun shouldInterceptRequest(
    view: WebView?,
    request: WebResourceRequest?
): WebResourceResponse? {
    var webResourceResponse: WebResourceResponse? = null

    filterRequest(view!!, request)

    if (shouldBlock) {
        Log.d("filtro", "interceptRequest NULL")
        webResourceResponse = WebResourceResponse("text/javascript", "UTF-8", null)
    }
    if (!shouldBlock) {
        view!!.post(Runnable {
            view.loadUrl(BrowserAPIProvider.filter.loadFilterUrl)
            webResourceResponse = null
        })

    }

    return webResourceResponse
}

The main problem I'm having is that the calls are produced in an infinite loop, the web doesn't load and many requests are made per second.

This is the current state of the code, however some lines of code come from changes and tests, I don't know how to develop the asynchrony with the api in the webview overridden methods, this is the biggest problem.

I would appreciate any kind of help and if you need more code I'll edit the question and copy it. Thank you very much in advance for your help.


Solution

  • This solution avoid deep bugs in the event handling of the WebView component.

    I've moved the api call to the web client, which has made it easier to implement the api calls. I will try to explain the solution in the best way that I can:

    The shouldOverrideUrlLoading method is the first to be called in the client that I have implemented the api call with, it has the advantage that the method is called each new load of the webview component either through webview.loadUrl() or through internal navigation of the component:

         override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
            nextUrl = leanGoogleNextUrl(request.url.toString())
            cleanGoogleNextUrl(nextUrl)
            if (!yetFilter && !shouldLoad) {
                view.post(Runnable {
                    kotlin.run {
                        view.stopLoading()
                        filterRequest(view)
                                      
                    }
                })
            }
            return false
        }
    

    These are the methods that call the API, the first one uses a callback since I needed to control the filter by means of a structure with two boolean values that complement the way in which the filter is established since one of the problems that I have faced is that the Loading each web component recursively goes through some of the methods of the webview client.

         private fun onAirFilter(
            url: String,
            filterCallback: (filtered: Boolean, mustFilter: Boolean) -> Unit
        ) {
            if (!ActiveTabs.forcedTabsMode) {
                Application().applicationScope.launch(Dispatchers.Main) {
                    val urlToTest = checkSafeBrowsing(url)
                    val result = filterUseCase.invoke(urlToTest)
                    if (result.isSuccessful) {
                        if (result.body()!!.result == "000") {
                            filterCallback.invoke(true, false)
                        } else {
                            filterCallback.invoke(true, true)
                        }
                        shouldLoad = true
                    }
                }
            }
        }
    
        private fun filterRequest(view: WebView) {
            view.post(Runnable {
                kotlin.run {
                    if (!yetFilter) {
                        view.stopLoading()
                        onAirFilter(nextUrl) { filtered, mustFilter ->
                            if (!mustFilter) {
                                view.loadUrl(nextUrl)
    
                            } else {
                                loadLazarusErrorPage(nextUrl, "1", view)
                            }
                            setYetFilterState(filtered)
                        }
                    }
                }
            })
        }
    
        private fun setYetFilterState(state: Boolean) {
            yetFilter = state
        }
    

    The last method of the webview client that I consider important is the one in which the variable that controls the need to call the api is reset.

        override fun shouldInterceptRequest(
            view: WebView?,
            request: WebResourceRequest?
        ): WebResourceResponse? {
            if (shouldLoad) {
                setYetFilterState(false)
            }
            return super.shouldInterceptRequest(view, request)
    
        }
    

    As always, it can be improved a lot and I appreciate any tip or advice to improve it.