The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


Advanced Uses of WebView

Android uses the WebKit browser engine as the foundation for both its Browser application and the WebView embeddable browsing widget. The Browser application, of course, is something Android users can interact with directly; the WebView widget is something you can integrate into your own applications for places where an HTML interface might be useful.

Earlier in this book, we saw a simple integration of a WebView into an Android activity, with the activity dictating what the browsing widget displayed and how it responded to links.

Here, we will expand on this theme, and show how to more tightly integrate the Java environment of an Android application with the JavaScript environment of WebKit.

Prerequisites

Understanding this chapter requires that you have read the core chapters, particularly the one covering WebView. Some of the samples use LocationManager for obtaining a GPS fix.

Friends with Benefits

When you integrate a WebView into your activity, you can control what Web pages are displayed, whether they are from a local provider or come from over the Internet, what should happen when a link is clicked, and so forth. And between WebView, WebViewClient, and WebSettings, you can control a fair bit about how the embedded browser behaves. Yet, by default, the browser itself is just a browser, capable of showing Web pages and interacting with Web sites, but otherwise gaining nothing from being hosted by an Android application.

However, WebView offers a few options for more tightly integrating the Java and JavaScript realms, so Web content can call into your app to get data to display, and your app can push data into the Web page for JavaScript to render.

Unfortunately, the techniques for doing this have changed over the years. Partially that is due to changes in WebView, particularly starting with Android 4.4. But, some of the changes are due to security issues, particularly when you are loading arbitrary content, such as Web-based ads from an ad network, into your WebView.

The following sections will go over four separate sample apps. All do the same thing: provide data about the ambient light level, using the sensor on the phone or tablet, to the Web page for rendering. The differences are whether we are pushing data from Java into JavaScript (e.g., as the light level changes), or whether we are pulling data from Java using JavaScript (e.g., in response to a user tapping on something in the Web page). Also, we will see two approaches to push and two approaches to pull.

JavaScript Calling Java: addJavascriptInterface()

The addJavascriptInterface() method on WebView allows you to inject a Java object into the WebView, exposing its methods, so they can be called by JavaScript loaded by the Web content in the WebView itself.

Now you have the power to provide access to a wide range of Android features and capabilities to your WebView-hosted content. If you can access it from your activity, and if you can wrap it in something convenient for use by JavaScript, your Web pages can access it as well.

The WebKit/SensorPull sample project demonstrates using addJavascriptInterface() to pull light sensor data into a Web page to display to the user.

For all four of these sample apps, the UI is just a WebView:

<?xml version="1.0" encoding="utf-8"?>
<WebView android:id="@+id/webkit"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

</WebView>

In onCreate(), we set things up:

  @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mgr=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
    light=mgr.getDefaultSensor(Sensor.TYPE_LIGHT);

    wv=(WebView)findViewById(R.id.webkit);
    wv.getSettings().setJavaScriptEnabled(true);
    wv.addJavascriptInterface(jsInterface, "LIGHT_SENSOR");
    wv.loadUrl("file:///android_asset/index.html");
  }

Specifically, we:

Because we are enabling JavaScript, Lint will complain that this poses security risks, so onCreate() has a @SuppressLint annotation for SetJavaScriptEnabled to indicate that we are aware of the risks. Similarly, because we are calling addJavascriptInterface(), Lint will complain that this poses even more security risks. So, @SuppressLint suppresses both the SetJavaScriptEnabled warning and the AddJavascriptInterface warning.

Also, you may notice that there is a significant debate within the Android SDK as to whether the “s” in “JavaScript” gets capitalized or not. In general, it does, but addJavascriptInterface() shipped in API Level 1 with a lowercase “s” in its name, and so that method, and variations of it (e.g., the AddJavascriptInterface annotation) will use a lowercase “s”. Eventually, you just get used to this.

addJavascriptInterface() takes two parameters: a Java object to inject into the JavaScript of the Web page, and a String that is the name by which JavaScript can reference that object. So, we have a jsInterface object that JavaScript can reference via LIGHT_SENSOR.

jsInterface is an instance of JSInterface, a static nested class inside MainActivity:

  private static class JSInterface {
    float lux=0.0f;

    private void updateLux(float lux) {
      this.lux=lux;
    }

    @JavascriptInterface
    public String getLux() {
      return(String.format(Locale.US, "{\"lux\": %f}", lux));
    }
  }

It just has a getter and setter around the current light level, which is a float named lux (referring to the unit of brightness used for the values coming from the ambient light sensor). The getter, however, has two interesting traits:

This annotation is required of apps with android:targetSdkVersion set to 17 or higher, though it is a good idea to start using it anyway. With such an android:targetSdkVersion, in an app running on an Android 4.2 or higher device, only public methods with the @JavascriptInterface annotation will be accessible by JavaScript code. On earlier devices, or with an earlier android:targetSdkVersion, all public methods on the JsInterface object would be accessible by JavaScript, including those inherited from superclasses like Object. Note that your build target (i.e., compileSdkVersion in Android Studio) will need to be Android 4.2 or higher in order to reference the @JavascriptInterface annotation.

The reason for returning a JSON object (in string form), rather than just the float, is for two reasons:

  1. For more complex APIs, you cannot pass into JavaScript an arbitrary Java object. All return types from @JavascriptInterface objects need to be something that JavaScript can use, and a simple way to do that is to create data structures in JSON.
  2. float did not seem to work well as a return type, as it always seemed to turn into 0.0 on the JavaScript side, for unknown reasons

We register with the SensorManager to find out when the light level changes, via registerListener() (in onStart()) and unregisterListener() (in onStop()):

  @Override
  protected void onStart() {
    super.onStart();

    mgr.registerListener(this, light, SensorManager.SENSOR_DELAY_UI);
  }

  @Override
  protected void onStop() {
    mgr.unregisterListener(this);

    super.onStop();
  }

That, in turn, will trigger a call to onSensorChanged() when the light level changes. There, we pass the light level (the first float out of the values array from the SensorEvent) to the JsInterface instance, ready to be retrieved by the JavaScript code in our Web page:

  @Override
  public void onSensorChanged(SensorEvent sensorEvent) {
    jsInterface.updateLux(sensorEvent.values[0]);
  }

  @Override
  public void onAccuracyChanged(Sensor sensor, int i) {
    // unused
  }

In the Web page, we set it up to show the current light level, starting with a value of 0.0. When the user taps on the Light Level caption, we call a pull() JavaScript function, which:

<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
  function update_lux(lux) {
    document.getElementById("lux").innerHTML=lux;
  }
  
  function pull() {
    var result=JSON.parse(LIGHT_SENSOR.getLux());

    update_lux(result.lux);
  }
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>

If you run the app, you get our trivial Web page in the WebView:

SensorPull Demo, As Initially Launched
Figure 543: SensorPull Demo, As Initially Launched

Tapping on the words “Light Level” will cause JavaScript to request the light level, updating the page to match:

SensorPull Demo, Showing Light Level
Figure 544: SensorPull Demo, Showing Light Level

Note that this sample app will only work on devices with an ambient light sensor. It is rather likely that the app will crash spectacularly on devices lacking such a sensor.

Unfortunately, addJavascriptInterface() opens up a number of security issues, outlined later in this chapter. Where possible, avoid the use of this API.

Java Calling JavaScript: loadUrl() and evaluateJavascript()

Now that we have seen how JavaScript can call into Java, it would be nice if Java could somehow call out to JavaScript. Well, as luck would have it, we can do that too. This is a good thing, otherwise, this would be a really weak section of the book.

What is unusual is how you call out to JavaScript. One might imagine there would be an evaluateJavaScript() counterpart to addJavascriptInterface(), where you could supply some JavaScript source and have it executed within the context of the currently-loaded Web page.

Actually, there is such a method on Android 4.4. However, earlier versions of Android lacked that method. Instead, on older versions of Android, given your snippet of JavaScript source to execute, you call loadUrl() on your WebView, as if you were going to load a Web page, but you put javascript: in front of your code and use that as the “address” to load.

If you have ever created a “bookmarklet” for a desktop Web browser, you will recognize this technique as being the Android analogue – the javascript: prefix tells the browser to treat the rest of the address as JavaScript source, injected into the currently-viewed Web page.

The WebKit/SensorPush sample project expands upon the SensorPull app. This time, though, in addition to pulling via addJavascriptInterface(), we support pushing light levels as sensor readings come in.

That comes courtesy of a revised onSensorChanged() method:

  @Override
  public void onSensorChanged(SensorEvent sensorEvent) {
    float lux=sensorEvent.values[0];

    jsInterface.updateLux(lux);

    String js=String.format(Locale.US, "update_lux(%f)", lux);

    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
      wv.evaluateJavascript(js, null);
    }
    else {
      wv.loadUrl("javascript:"+js);
    }
  }

Before, we just updated the JsInterface object with the new light level. Now, we also format a JavaScript call to update_lux(), supplying our light level. Then, based on Android OS version (Build.VERSION.SDK_INT), we either call evaluateJavascript() or loadUrl(), the latter also employing the javascript: scheme.

Because we had pulled out update_lux() as a separate function before, our HTML and JavaScript does not need to change at all:

<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
  function update_lux(lux) {
    document.getElementById("lux").innerHTML=lux;
  }
  
  function pull() {
    var result=JSON.parse(LIGHT_SENSOR.getLux());

    update_lux(result.lux);
  }
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>

If you run this sample app, you will find that the Web page updates in real time as you wave your hand in front of the light sensor, shine a light on that sensor, etc.

Java Calling JavaScript: WebMessage

Both of those techniques have worked since API Level 1. But, as mentioned, addJavascriptInterface() has security issues. Also, evaluateJavascript() (or its loadUrl() equivalent) requires the Java code to know what functions are available in the Web page. That may tie the Java and JavaScript more tightly than you might like.

Android 6.0 introduced another pair of options for communicating between Java and JavaScript — WebMessage and WebMessagePort — that try to eliminate these issues.

The simpler of the two approaches is WebMessage. Instead of calling evaluateJavascript() or loadUrl(), you create a WebMessage object and call postWebMessage() to deliver it to the JavaScript in your Web page. So, in the WebKit/SensorMessage sample project, we have an updated onSensorChanged() method that does this:

  @Override
  public void onSensorChanged(SensorEvent sensorEvent) {
    float lux=sensorEvent.values[0];

    jsInterface.updateLux(lux);

    String js=String.format(Locale.US, "update_lux(%f)", lux);

    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
      wv.postWebMessage(new WebMessage(jsInterface.getLux()),
        Uri.EMPTY);
    }
    else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
      wv.evaluateJavascript(js, null);
    }
    else {
      wv.loadUrl("javascript:"+js);
    }
  }

The WebMessage constructor that we are using here takes a simple string that is the content of the message. In this case, it is the JSON object wrapping our light level in lux, using the same getLux() method that JavaScript can call on our JsInterface instance that we registered via addJavascriptInterface().

postWebMessage() takes two parameters. The first is the WebMessage to deliver to the page. The other is supposed to be the Uri of the Web page. This is supposed to be used to confirm that you are sending the message to the page that you think you are sending it to.

Unfortunately, this is not behaving especially well.

It only works as advertised for http/https URLs, or for data that you load using loadDataWithBaseURL() and supply some http or https URL. If you load from a file URL, as we are doing here, you cannot use the actual URL. Instead, you have to use Uri.EMPTY, which is a “wildcard” that skips over this test, which is what we use here. Apparently, this is all working as intended.

To receive these messages, our JavaScript needs to define an onmessage() global function:

<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
  function update_lux(lux) {
    document.getElementById("lux").innerHTML=lux;
  }

  function parse(json) {
    var result=JSON.parse(json);

    update_lux(result.lux);
  }
  
  function pull() {
    parse(LIGHT_SENSOR.getLux());
  }

  onmessage = function (e) {
    parse(e.data);
  }
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>

This receives the HTML Web message equivalent of the WebMessage that we posted. The data field on the supplied event object (e in the sample) contains our string. So, we turn around and parse() it, just as we would parse() the JSON we got from calling getLux() on our LIGHT_SENSOR.

If you run this sample app on an Android 6.0+ device, you should get the same results as with SensorPush, where the light level changes automatically. However, in this case, we will be using code that relies upon WebMessage and postWebMessage(), instead of evaluateJavascript() or loadUrl(). In particular, our Java code does not need to know anything about the internal workings of the JavaScript (e.g., function names) — it just passes over the message and relies on the JavaScript to have registered itself appropriately to receive the message.

JavaScript Calling Java: WebMessagePort

What would be nice is to use this WebMessage system to be able to replace addJavascriptInterface() and allow JavaScript to call back into Java. This is possible, but it is fairly complex.

For our Java code to receive messages sent to it from JavaScript, we need to do three things:

  1. Call createWebMessageChannel() on the WebView. This creates a private communications channel between us and our target Web page. It returns a two-element WebMessagePort array. Index 0 of that array is our end of the channel; index 1 is the JavaScript end of the channel.
  2. Call setWebMessageCallback() on our WebMessagePort, supplying a WebMessageCallback that will be called with onMessage() when a message arrives on the port from JavaScript.
  3. Send the other WebMessagePort to JavaScript using a WebMessage.

In the WebKit/SensorPort sample project, this is handled by an initPort() method:

  @TargetApi(Build.VERSION_CODES.M)
  private void initPort() {
    final WebMessagePort[] channel=wv.createWebMessageChannel();

    port=channel[0];
    port.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
      @Override
      public void onMessage(WebMessagePort port, WebMessage message) {
        postLux();
      }
    });

    wv.postWebMessage(new WebMessage("", new WebMessagePort[]{channel[1]}),
          Uri.EMPTY);
  }

The WebMessage constructor that we use this time takes two parameters: an arbitrary string (here, just set to "", as we are not using it) and a one-element WebMessagePort array containing the JavaScript end of the communications channel.

However, we cannot do any of this work until the Web page is ready to be used. Otherwise, the JavaScript code will not receive our WebMessage, since it is not yet ready.

So, we have to postpone calling initPort() until a WebViewClient is called with onPageFinished(), as we do in onCreate():

  @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mgr=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
    light=mgr.getDefaultSensor(Sensor.TYPE_LIGHT);

    wv=(WebView)findViewById(R.id.webkit);
    wv.getSettings().setJavaScriptEnabled(true);
    wv.addJavascriptInterface(jsInterface, "LIGHT_SENSOR");

    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
      wv.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
          initPort();
        }
      });
    }

    wv.loadUrl(URL);
  }

Our WebMessageCallback, upon receipt of a WebMessage from the JavaScript, calls a postLux() method. We are just using the existence of the message as a “ping” from the JavaScript to Java, asking for us to send it the ambient light level. So, in postLux(), we create a WebMessage and send it to JavaScript… but not via the postWebMessage() method on WebView. Instead, we use our end of the WebMessagePort communications channel, calling postMessage() on it, in a postLux() method:

  @TargetApi(Build.VERSION_CODES.M)
  private void postLux() {
    port.postMessage(new WebMessage(jsInterface.getLux()));
  }

So, when the JavaScript sends a WebMessage to Java, Java sends a WebMessage right back, supplying the light level JSON.

To prove that this is working, this sample comments out the automated push of the light level in onSensorChanged() — ordinarily, we would call postLux() to push over the light level when we get it:

  @Override
  public void onSensorChanged(SensorEvent sensorEvent) {
    float lux=sensorEvent.values[0];

    jsInterface.updateLux(lux);

    String js=String.format(Locale.US, "update_lux(%f)", lux);

    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) {
      // postLux();
    }
    else if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT) {
      wv.evaluateJavascript(js, null);
    }
    else {
      wv.loadUrl("javascript:"+js);
    }
  }

In JavaScript, our onmessage global function is now a bit more complex as well. We get our end of the communications channel by retrieving our port from ports on the event delivered to onmessage(). Then, we register an onmessage function on that port, which is how we receive the light levels that Java delivers via postMessage() on its WebMessagePort. When the user taps the label, we call a pull() function in JavaScript, that calls postMessage() on the port, supplying some string as a message (here, hardcoded as "ping" and ignored by our Java code):

<html>
<head>
<title>Android Light Sensor Demo</title>
<script language="javascript">
  function update_lux(lux) {
    document.getElementById("lux").innerHTML=lux;
  }

  function parse(json) {
    var result=JSON.parse(json);

    update_lux(result.lux);
  }

  var port;

  function pull() {
    port.postMessage("ping");
  }

  onmessage = function (e) {
    port = e.ports[0];

    port.onmessage = function (f) {
      parse(f.data);
    }
  }
</script>
</head>
<body>
<p><b><a onClick="pull()">Light Level</a>: </b><span id="lux">0.0</span> lux</p>
</body>
</html>

If you run this sample on Android 6.0+, and you tap the “Light Level” label, you will get the light level, delivered by means of our WebMessagePort-based communications channel.

(NOTE: the author would like to thank Diego Torres Milano for his assistance in finding out how this stuff works).

Navigating the Waters

The preview of this section was abducted by space aliens.

Settings, Preferences, and Options (Oh, My!)

The preview of this section was lost in the sofa cushions.

Security and Your WebView

The preview of this section was fed to a gremlin, after midnight.

Android 8.0 WebView Changes

The preview of this section is [REDACTED].

Chrome Custom Tabs

The preview of this section is out seeking fame and fortune as the Dread Pirate Roberts.

Prerequisites

The preview of this section is being chased by zombies.

Keyboards, Hard and Soft

The preview of this section was lost in the sofa cushions.

Tailored To Your Needs

The preview of this section took that left turn at Albuquerque.

Tell Android Where It Can Go

The preview of this section is being chased by zombies.

Fitting In

The preview of this section was fed to a gremlin, after midnight.

Jane, Stop This Crazy Thing!

The preview of this section is [REDACTED].