How Do You Use WebMessagePort As An Alternative to addJavascriptInterface()?

There's a test for it in CTS

// Create a message channel and make sure it can be used for data transfer to/from js.
public void testMessageChannel() throws Throwable {
    if (!NullWebViewUtils.isWebViewAvailable()) {
        return;
    }
    loadPage(CHANNEL_MESSAGE);
    final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
    WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
    mOnUiThread.postWebMessage(message, Uri.parse(BASE_URI));
    final int messageCount = 3;
    final CountDownLatch latch = new CountDownLatch(messageCount);
    runTestOnUiThread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < messageCount; i++) {
                channel[0].postMessage(new WebMessage(WEBVIEW_MESSAGE + i));
            }
            channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
                @Override
                public void onMessage(WebMessagePort port, WebMessage message) {
                    int i = messageCount - (int)latch.getCount();
                    assertEquals(WEBVIEW_MESSAGE + i + i, message.getData());
                    latch.countDown();
                }
            });
        }
    });
    // Wait for all the responses to arrive.
    boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}

file: cts/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java. At least some starting point.


Here's a solution using the compat lib: Download Full Solution in Android Studio format

This example uses an index.html and an index.js file stored in the assets folder.

Here is the JS:

const channel = new MessageChannel();
var nativeJsPortOne = channel.port1;
var nativeJsPortTwo = channel.port2;
window.addEventListener('message', function(event) {
    if (event.data != 'capturePort') {
        nativeJsPortOne.postMessage(event.data)
    } else if (event.data == 'capturePort') {
        /* The following three lines form Android class 'WebViewCallBackDemo' capture the port and assign it to nativeJsPortTwo
        var destPort = arrayOf(nativeToJsPorts[1])
        nativeToJsPorts[0].setWebMessageCallback(nativeToJs!!)
        WebViewCompat.postWebMessage(webView, WebMessageCompat("capturePort", destPort), Uri.EMPTY) */
        if (event.ports[0] != null) {
            nativeJsPortTwo = event.ports[0]
        }
    }
}, false);

nativeJsPortOne.addEventListener('message', function(event) {
    alert(event.data);
}, false);

nativeJsPortTwo.addEventListener('message', function(event) {
    alert(event.data);
}, false);
nativeJsPortOne.start();
nativeJsPortTwo.start();

And here is the HTML:

<!DOCTYPE html>
<html lang="en-gb">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebView Callback Demo</title>
    <script src="js/index.js"></script>
</head>
<body>
    <div style="font-size: 24pt; text-align: center;">
        <input type="button" value="Test" onclick="nativeJsPortTwo.postMessage(msgFromJS.value);" style="font-size: inherit;" /><br />
        <input id="msgFromJS" type="text" value="JavaScript To Native" style="font-size: 16pt; text-align: inherit; width: 80%;" />
    </div>
</body>
</html>

And finally Here is the native Android code:

class PostMessageHandler(webView: WebView) {
    private val nativeToJsPorts = WebViewCompat.createWebMessageChannel(webView)
    private var nativeToJs: WebMessagePortCompat.WebMessageCallbackCompat? = null
    init {
        if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_CALLBACK_ON_MESSAGE)) {
            nativeToJs = object : WebMessagePortCompat.WebMessageCallbackCompat() {
                override fun onMessage(port: WebMessagePortCompat, message: WebMessageCompat?) {
                    super.onMessage(port, message)
                    Toast.makeText(webView.context, message!!.data, Toast.LENGTH_SHORT).show()
                }
            }
        }
        var destPort = arrayOf(nativeToJsPorts[1])
        nativeToJsPorts[0].setWebMessageCallback(nativeToJs!!)
        WebViewCompat.postWebMessage(webView, WebMessageCompat("capturePort", destPort), Uri.EMPTY)
    }
}

It is important the the native code be executed from the 'WebViewClient.onPageFinished(webView: WebView, url: String)' callback. See download link above for full details. This project shows postMessage working both ways (Native to JS and JS to Native) Hope this helps.


OK, I have this working, though it kinda sucks.

Step #1: Populate your WebView using loadDataWithBaseURL(). loadUrl() will not work, because bugs. You need to use an http or https URL for the first parameter to loadDataWithBaseURL() (or, at least, not file, because bugs). And you will need that URL later, so hold onto it (e.g., private static final String value).

Step #2: Decide when you want to initialize the communications from the JavaScript into Java. With addJavascriptInterface(), this is available immediately. However, using WebMessagePort is not nearly so nice. In particular, you cannot attempt to initialize the communications until the page is loaded (e.g., onPageFinished() on a WebViewClient).

Step #3: At the time that you want to initialize those communications, call createWebMessageChannel() on the WebView, to create a WebMessagePort[]. The 0th element in that array is your end of the communications pipe, and you can call setWebMessageCallback() on it to be able to respond to messages sent to you from JavaScript.

Step #4: Hand the 1st element in that WebMessagePort[] to the JavaScript by wrapping it in a WebMessage and calling postWebMessage() on the WebView. postWebMessage() takes a Uri as the second parameter, and this Uri must be derived from the same URL that you used in Step #1 as the base URL for loadDataWithBaseURL().

  @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.parse(THIS_IS_STUPID));
  }

(where wv is the WebView and THIS_IS_STUPID is the URL used with loadDataWithBaseURL())

Step #5: Your JavaScript can assign a function to the global onmessage event, which will be called when postWebMessage() is called. The 0th element of the ports array that you get on the event will be the JavaScript end of the communications pipe, and you can stuff that in a variable somewhere. If desired, you can assign a function to onmessage for that port, if the Java code will use the WebMessagePort for sending over future data.

Step #6: When you want to send a message from JavaScript to Java, call postMessage() on the port from Step #5, and that message will be delivered to the callback that you registered with setWebMessageCallback() in step #3.

var port;

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

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

    port.onmessage = function (f) {
        parse(f.data);
    }
}

This sample app demonstrates the technique. It has a WebView that shows the current light level based on the ambient light sensor. That sensor data is fed into the WebView either on a push basis (as the sensor changes) or on a pull basis (user taps the "Light Level" label on the Web page). This app uses WebMessagePort for these on Android 6.0+ devices, though the push option is commented out so you can confirm that the pull approach is working through the port. I will have more detailed coverage of the sample app in an upcoming edition of my book.