Can the PostMessage API be used to communicate with an Android WebView?

21,262

Solution 1

I realize this question is old but I ran into it so I figured I would answer here. In short - I am finding that postMessage does work at least for communication from a child iframe to a parent window BUT...

Turns out we really didn't like the way the iframe behaved in android's WebView so we rendered the contents of the iframe directly instead (as you suggest). This left us with two problems - first we had lots of messaging hooks from that iframe to it's parent and second we still needed to call out to android to react to these events.

Here's an example message from our code - which was sprinkled throughout the iframe:

    parent.postMessage(JSON.stringify({
        action    : 'openModal',
        source    : embedId
    }), '*');

When we're on Android what we want is to use android's support for javascript interfaces to inject an object to handle this request when running in a WebView.

On the Android side this will look something like this:

class JsObject {
   @JavascriptInterface
    public boolean postMessage(String json, String transferList) {
        return false; // here we return true if we handled the post.
    }
}

// And when initializing the webview... 
webView.addJavascriptInterface(new JsObject(), "totDevice");

Now when running inside this WebView totDevice will exist and when running in an iframe it won't. So now we can create a wrapper to check for this condition and cleanly switch between the two methods rather than calling parent.postMessage directly. Here we also added a boolean switch in our Android implementation in case you only wanted to handle some of the messages:

function postMessage(parent, json, transferlist) {
    if (!totDevice || !totDevice.postMessage(json, transferList)) {
        parent.postMessage(json, transferlist);
    }
}

Our original postMessage from above can be rewritten:

    postMessage(parent, JSON.stringify({
        action    : 'openModal',
        source    : embedId
    }), '*');

Now we have a single set of code that can run in an iframe or android WebView with no change (at least to this part of the code).

I hope that helps someone.

Solution 2

Recently we had to develop a project that needed to communicate our native Android app with an external webview integration from a third party.

The idea that this question raises in stackoverflow is very interesting, more so if you can't touch the JS code of that webview.

I describe what we did to be able to communicate the webview with the native app through the message steps via the JS PostMessage API.

Using our webview implementation. We implemented the onPageFinished method in order to inject our JS code to load the web.

override fun onPageFinished(url: String?) {
webview.loadUrl(
"javascript:(function() {" +
    "window.parent.addEventListener ('message', function(event) {" +
    " Android.receiveMessage(JSON.stringify(event.data));});" +
    "})()"
)
}

Basically what we are doing is creating a listener that sends those messages to our own JS and Android Bridge interface. Which we've previously created in the webview setup in our Android activity as we normally do with addJavascriptInterface

webview.addJavascriptInterface(JsObject(presenter), "Android”)

This way, we already have that communication bridge and all the messages sent by the postMessage will reach us in that interface that is subscribed with that listener.

class JsObject(val presenter: Presenter) {
    @JavascriptInterface
    fun receiveMessage(data: String): Boolean {
        presenter.onDataReceived(data)
        Log.d("Data from JS", data)
        return true
    }

Solution 3

I was facing similar issue, I wanted to listen to .postMessage event occuring in webview. I checked in raw html that the event was fired like this

window.parent.postMessage(JSON.stringify(message), '*');

Hence I dug more into postMessage and found this link for adding eventListener

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event)
{
  if (event.origin !== "http://example.org:8080")
    return;

  // ...
}

durban's answer helped me for setting up Android JS Interface.

Firstly a custom class for JavascriptInterface like this

class JsObject {
    @JavascriptInterface
    public void receiveMessage(String data) {
        Log.i("JsObject", "postMessage data="+data);
        //handle data here
    }
}

Add javascriptInterface to webview before loading url/html

webView.addJavascriptInterface(new JsObject(), "Android"); //"Android" is just a name

Then call javascript in the onPageStarted callback of WebViewClient like below

  @Override
  public void onPageStarted(WebView view, String url, Bitmap favicon) {
        webView.loadUrl("javascript:(function() {" +
                            "function receiveMessage(event) {\n" +
                            "Android.receiveMessage(JSON.stringify(event.data));\n" +
                            "}" +
                            "window.addEventListener(\"message\", receiveMessage, false);"+
                            "})()"
        );
        Log.i(TAG, "onPageStarted "+url);
  }

Solution 4

The answers here are good answers at the time they were posted, but now that androidx.webkit is available I believe that is the recommended approach.

In particular, WebViewCompat.addWebMessageListener and WebViewCompat.postWebMessage would be the counterparts to the javascript PostMessage API.

Here is the code example copied from the documentation:

 // Web page (in JavaScript)
 myObject.onmessage = function(event) {
   // prints "Got it!" when we receive the app's response.
   console.log(event.data);
 }
 myObject.postMessage("I'm ready!");
 
 // App (in Java)
 WebMessageListener myListener = new WebMessageListener() {
   @Override
   public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
            boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
     // do something about view, message, sourceOrigin and isMainFrame.
     replyProxy.postMessage("Got it!");
   }
 };
 if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
   WebViewCompat.addWebMessageListener(webView, "myObject", rules, myListener);
 }

Solution 5

You need to use an intermediate abstract interface that in one implementation processes messages via PostMessage and in another case via addJavascriptInterface.

window.addEventListener("message", onReceivedPostMessage, false);

function onReceivedPostMessage(event){
     //..ex deconstruct event into action & params
     var action = event.data.action;
     var params = event.data.params;
     performAction(action, params); //performAction would be the uniform API
}

function onReceivedActivityMessageViaJavascriptInterface(json){
     //..ex deconstruct data into action & params
     var data = JSON.parse(json); 
     var action = data.action;
     var params = data.params;
     performAction(action, params); //performAction would be the uniform API
}
Share:
21,262
Brian Putnam
Author by

Brian Putnam

Updated on September 09, 2021

Comments

  • Brian Putnam
    Brian Putnam over 2 years

    I usually use the HTML5 PostMessage API to communicate information from my iframed content to the parent frame. Recently I've had my content used inside an Android WebView (as far as I can tell this is the native-Android equivalent of an iframe). Is there a way for the native app to listen for PostMessage events that I send up to them?

    I'm aware that addJavascriptInterface exists, I'm just hoping that there's a way to reuse my existing PostMessage code without writing something new.