The Adventures of the Premier Online Cinema on Android TV, or How We Implemented Javascript

Since there was no support for Android TV, in particular navigation using the remote control, inside the library, I decided to use the web version of the library and a custom interface with Android TV support.

What came of it – read on. The article will be useful for those who like bold experiments, work with Android or Android TV and know what Javascript is.

Someone did exactly that…

Deciding to use the web version of the library, I started looking for the right tool to do what I wanted to do.

The first step I decided to pay attention to large frameworks. The choice fell on Rhino from Mozilla. Rhino is a tool with almost limitless possibilities: executing code, including libraries, creating an interface, it seems that five minutes and it’s in the bag it is ideal for integration.

However, when I went to the framework website, I found a friendly “Page not found”. But it was too early to despair – later I still managed to find a “live” repository. In addition, there were also more “native” adaptations of Rhino for Android – F43nd1r/rhino-android

Later it turned out that Rhino does not have the ability to dynamically load libraries. You can add the library to Rhino via npm or, more simply, add the min.js file downloaded in advance to the project. But in our case, the js library that we implemented had to be re-downloaded from the server each time. That is, I did not have the opportunity to add the min.js library file to the project, and I had to abandon the idea of ​​using Rhino.

After giving up on Rhino, I continued my search. I found out that there are a number of self-written solutions that use WebView as an “engine” under the hood. For example, here it is: evgenyneu/js-evaluator-for-android. These libraries allow you to execute simple js expressions inside a WebView. But such solutions also lack the ability to connect libraries. And this meant only one thing for us – this solution still does not suit us. “Is everything gone, chief?”

Stage five. Adoption

After several unsuccessful attempts to find a ready-made solution that would completely suit me, I decided to try to implement my own small “framework”. I took WebView as the basis of the “framework”, since many of the necessary functions are already there out of the box. And then I will try to tell in detail how you can turn a simple WebView into a JS interpreter.

Step one. Training

First we need to prepare the WebView for our purposes. At this stage, there are a few things to consider:

  • To use an object, you need to create it 🙂

  • In the WebView settings, you need to enable the ability to execute JS scripts

  • To avoid unexpected caching issues, the WebView should have its cache disabled. So we will definitely be sure that all operations are performed cleanly and in the context of the page there are no tails left from past calls.

Below is an example of creating a WebView with all the necessary settings:

//Создаем WebView
val webView = WebView(context)

with(webView) {
		//Разрешаем исполнение JavaScript кода
    settings.javaScriptEnabled = true
		//Отключаем кэш у webView
    settings.cacheMode = WebSettings.LOAD_NO_CACHE
}

Now that we have everything we need set up, we can move on to creating our JS scripts.

Step two. Preparing JS scripts

To add our first JS script to the project, we need to create an html file in which it will be located. The html file must be placed in the assets directory inside the project. WebView will work with this file, namely, it will load it as a local html page, loading all the necessary dependencies.

Then you need to add the standard html page structure to the file – head tags, body tags, etc. (see example). After creating an empty page, you can proceed to adding the scripts themselves. Scripts are connected in a standard way for html – through the use of the

Let’s name our file sample.html. Inside it, we will include the moment.js library and create a checkMoment function that will return the current time in string format.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>

<script
        src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"
        integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ=="
        crossorigin="anonymous"
        referrerpolicy="no-referrer"
></script>

<script type="text/javascript">
            function checkMoment() {
                return moment().toString();
            }

</script>
</body>
</html>

Step three. Running the first script

WebView is configured, scripts are created – it’s time to move on to launch.

In order to run the js code, we first need to load our page with scripts in a WebView. But when loading the page, you need to take into account that the call to the js function must occur after the WebView loads the page and all its contents.

To determine when the WebView has finished loading data, you need to use the WebViewClient. We need two methods in it – onPageFinished and onReceivedError. The onPageFinished method is called when the WebView has finished loading data, and onReceivedError signals that an error occurred during the loading process.

webView.webViewClient = object : WebViewClient() {
	  override fun onPageFinished(view: WebView?, url: String?) {
	      super.onPageFinished(view, url)
	      //Страница загружена и готова к использованию
	  }
	
	  override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
	      super.onReceivedError(view, request, error)
	      //в процессе загрузки возникла  ошибка
	  }
}

Having loaded everything you need, we proceed (finally!) to the execution of our script.

WebView comes with evaluateJavaScript function out of the box. This function takes as an argument a js expression that will be executed in the current WebView context. That is, after we have loaded the html page with the connected library, we can access the library methods through the evaluateJavascript method. It will look like this:

webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        //Страница загружена и готова к использованию
				//вызываем js функцию checkMoment
        webView.evaluateJavascript("checkMoment()") { result ->
            Toast.makeText(requireContext(), "result: $result", Toast.LENGTH_SHORT).show()
        }

    }

    override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
        super.onReceivedError(view, request, error)
        //в процессе загрузки возникла  ошибка
    }
}
webView.loadUrl("file:///android_asset/sample.html")

After adding the webViewClient, the code above will call the webView’s loadUrl method, which will load our script page we created earlier. After the download is complete, the webViewClient.onPageFinished method will twitch, it will call the webView.evaluateJavascript method, which, in turn, will call our checkMoment function. The result of the checkMoment execution (remember that this is the current date and time concatenated into one string) will return to the callback and the final action will be a toast displaying the current date.

We spoke in detail the principle of work, remembered, put it on the shelves, we move on.

Let’s make it asynchronous, shall we?

The next question that confronted me: what to do if you need to make a request from the js code? And after all for the sake of it all and was started. The answer suggests itself – you need to write a wrapper that would allow you to perform the necessary operations asynchronously.

To do this, let’s create our own class, let’s call it JSClient. Let’s transfer WebView and settings for it to the new class.

class JSClient(context: Context) {
    val webView = WebView(context)

    init {
        with(webView){
            settings.javaScriptEnabled = true
            settings.cacheMode = WebSettings.LOAD_NO_CACHE
            webChromeClient = WebChromeClient()

        }
    }
}

As discussed earlier, before executing the js code, we need to load our page into the webView. To do this, we will create a suspend function inside our class, which will be responsible for preparing the webView for work. Let’s call it startConnection. Inside this function, we will place the webView loading code using the webViewClient from the previous paragraph.

suspend fun startConnection() = suspendCancellableCoroutine<Boolean?> { continuation ->
          webView.webViewClient = object : WebViewClient() {
              override fun onPageFinished(view: WebView?, url: String?) {
                  super.onPageFinished(view, url)
                  if (continuation.isActive) {
                      continuation.resume(true)
                  }
              }


              override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                  super.onReceivedError(view, request, error)
                  if (continuation.isActive) {
                      continuation.resumeWithException(RuntimeException())
                  }
              }
          }
          webView.loadUrl("file:///android_asset/sample.html")
      }
  }

Now imagine that the checkMoment function from the previous paragraph makes a request and can be executed for quite a long time. In this case, you need to create an asynchronous call option for it too.

suspend fun checkMoment() = suspendCoroutine<String> { continuation ->
    webView.evaluateJavascript("checkMoment()") { result ->
        when {
            !result.isNullOrEmpty() -> continuation.resume(result)
            else -> continuation.resumeWithException(Throwable())
        }
    }
}

Now let’s put everything together and execute the first asynchronous request.

val client = JSClient(context)
viewLifecycleOwner.lifecycleScope.launch {
    client.startConnection()
    val result = client.checkMoment()
    Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
}

In the code above, our JSClient class is initialized, then the startConnection function is called. This function prepares the webView for work and loads the scripts. After the startConnection finishes running, the asynchronous version of the checkMoment function is called, which still returns the current date and a toast is displayed on the screen.

Pros, cons, pitfalls

The next problem I ran into was executing multiple requests in a row. The previous solution has a big minus – to execute each request, you need to reload scripts and libraries. This is an extra traffic consumption, and it can take quite a lot of time (depending on the size and number of connected libraries). The answer to the question “what to do now?” lay on the surface. Before loading our page with scripts, we need to check whether they really need to be loaded or have they already been loaded before?

In order to check the need to load data, I added another function to our sample.html file – isScriptsLoaded. The main role of this function is to check if the library is inside the WebView.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>

<script
        src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"
        integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ=="
        crossorigin="anonymous"
        referrerpolicy="no-referrer"
></script>

<script type="text/javascript">
        function isScriptsLoaded() {
            return typeof moment === 'function';
        }

</script>
<script type="text/javascript">
            function checkMoment() {
                return moment().toString();
            }

</script>
</body>
</html>

In the code above, the isScriptsLoaded function compares the type of the moment method and the function using the typeof operator. This expression will be true if the library loaded successfully and the WebView is ready to go. If an error occurred during the loading process or the data was not loaded, the typeof operator will return ‘undefined’

Now let’s see how this feature will help us prevent unnecessary data reloads.

First, let’s add it to our startConnection function, before loading the WebView data.

suspend fun startConnection() = suspendCancellableCoroutine<Boolean?> { continuation ->
      webView.evaluateJavascript("isScriptsLoaded()") { result ->
          when(result) {
              "true" -> continuation.resume(true)

              else -> {
                  webView.webViewClient = object : WebViewClient() {
                      override fun onPageFinished(view: WebView?, url: String?) {
                          super.onPageFinished(view, url)
                          if (continuation.isActive) {
                              continuation.resume(true)
                          }
                      }


                      override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                          super.onReceivedError(view, request, error)
                          if (continuation.isActive) {
                              continuation.resumeWithException(RuntimeException())
                          }
                      }
                  }
                  webView.loadUrl("file:///android_asset/sample.html")
              }
          }
      }
  }

The usage and method of calling the startConnection function remains the same:

val client = JSClient(context)
viewLifecycleOwner.lifecycleScope.launch {
    client.startConnection()
    val result = client.checkMoment()
    Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
}

But now we have the opportunity, when calling startConnection, to determine whether the data really needs to be reloaded. After calling isScriptsLoaded , we determine whether the scripts are loaded (isScriptsLoaded returned “true”) or not (isScriptsLoaded returned “undefined”) and, on this basis, either return information that the webView is ready to work, or load the data again.

Conclusion

This is how the adventure called “integrating JS into an android application” ended. Using this approach, you can connect almost any JS library to the project. At the same time, integration does not require adding third-party dependencies to the project. I was glad to meet you, I hope that the article was useful to you. If you have any questions or questions, I invite everyone to continue the discussion in the comments!