Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Google's security guidelines for Android app developers has the following:

WebViews do not use addJavaScriptInterface() with untrusted content.

On Android M and above, HTML message channels can be used instead.

Near as I can tell, "HTML message channels" refers to things like createWebMessageChannel(), WebMessagePort, WebMessage, and kin.

However, they do not provide any examples. All they do is link to a WhatWG specification, which is rather unclear. And, based on a Google search for createWebMessageChannel, it appears that this has not been used much yet — my blog post describing changes in the Android 6.0 SDK makes the top 10 search results, and I just mention it in passing.

addJavascriptInterface() is used to allow JavaScript in a WebView to call into Java code supplied by the app using the WebView. How would we use "HTML message channels" as a replacement for that?

like image 516
CommonsWare Avatar asked Jan 19 '17 22:01

CommonsWare


2 Answers

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.

like image 135
CommonsWare Avatar answered Sep 27 '22 21:09

CommonsWare


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.

like image 25
Diego Torres Milano Avatar answered Sep 27 '22 21:09

Diego Torres Milano