0

There is a lot of discussion going on about Google not providing a Java (and other) API to access their GAE channels, which can currently only be accessed from JavaScript, as described here:

https://cloud.google.com/appengine/docs/java/channel/ https://cloud.google.com/appengine/docs/java/channel/javascript

Essentially, there are 2 possible approaches:

  1. Write Java classes which "simulate" the access, as done here: https://github.com/gvsumasl/jacc
  2. Use a hidden WebView and connect JavaScript to Java, as described here: http://developer.android.com/guide/webapps/webview.html

I tried option 1 and had this working (so I assume my server code is fine), but I rejected it because there is no guarantee that Google won't change their undocumented API in the future.

I went ahead and implemented the following code:

A Java class, which created the WebView and consumes JavaScript events:

public class ChannelService {

    class ChannelListener {
        @JavascriptInterface public void onOpen() {
            System.out.println("open"); // PROBLEM: these lines are never called
        }
        @JavascriptInterface public void onMessage(String message) {
            System.out.println("message");
        }
        @JavascriptInterface public void onError(Integer errorCode, String description) {
            System.out.println("error");
        }
        @JavascriptInterface public void onClose() {
            System.out.println("close");
        }
    }

    public ChannelService(Context context) throws IOException {
        final WebView webView = new WebView(context);
        webView.getSettings().setJavaScriptEnabled(true);

        webView.addJavascriptInterface(new ChannelListener(), "channelListener"); // the connection between Java and JavaScript

        webView.setWebViewClient(new WebViewClient() {
            @Override public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url); // this line is called after the page loads
            }
            @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
                super.onReceivedError(view, errorCode, description, failingUrl);
            }
        });

        String html = AssetsHelper.assetToString(context, "channels.html"); // loads the html file below

        html = html.replaceAll("\\{\\{ channelurl \\}\\}", ApiService.channelUrl()); // channelurl and token are replaced fine and should be correct
        html = html.replaceAll("\\{\\{ token \\}\\}", Session.getToken().getId());

        webView.loadData(html, "text/html", null);
    }

}

The channels.html file with JavaScript code:

<html>
    <head>
        <script src='{{ channelurl }}jsapi'></script>
    </head>
    <body>
        <script type='text/javascript'>

            onOpen = function() {
                channelListener.onOpen();
            }
            onMessage = function(message) {
                channelListener.onMessage(message);
            }
            onError = function(errorcode, description) {
                channelListener.onError(errorcode, description);
            }
            onClose = function() {
                channelListener.onClose();
            }

            openChannel = function() {
                var token = '{{ token }}';
                var channel = new goog.appengine.Channel(token);

                var handler = {
                    'onopen': onOpen,
                    'onmessage': onMessage,
                    'onerror': onError,
                    'onclose': onClose
                };

                var socket = channel.open(handler);

                socket.onopen = onOpen;
                socket.onmessage = onMessage;
                socket.onerror = onError;
                socket.onclose = onClose;
            }

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

Thanks to Google, the WebView does not report any errors with my code, which makes it very hard to debug. Thanks to myself I never actively wrote JavaScript code, so I am quite stupid here and you may laugh and spot my mistake immediately.

I have documented this in the code already, but let me state another time what's working and what not:

  • 95% sure that the server-side code works, as I had option 1 working. So I am almost certain that my messages are sent to the client.
  • I am pre-creating channels when the client signs on and pass the token to the client over REST endpoints. It gets there intact (compared it), and it is also embedded fine after replaceAll.
  • My website seems to load as I receive onPageFinished() with the correct url. (FIXED: I actually receive that event several times, with something that looks like params, but I guess that's Android and normal.)
  • Then nothing happens, no exception and the Java events aren't called.
  • My Java code continues, and when I set a breakpoint the WebView can be retrieved and is alive I used a public variable in my Activity for it, which should be fine; and I made sure it's on the UI thread.

Any ideas welcome, either what I did wrong or how I can possibly debug this. Thanks!

Oliver Hausler
  • 4,501
  • 4
  • 30
  • 62
  • Sorry, I made a mistake in my code. Instead of webView.loadData(html, "text/html", null); I loaded the data as url webView.loadUrl(html); - the reason why OnPageFinished was called twice. I have fixed this in the code above. The problem persists. – Oliver Hausler Jan 04 '15 at 16:48

1 Answers1

0

Two problems that I have found:

  1. The JavaScript code is never triggered, it's clear that nothing will happen.

Either you call openChannel from onPageFinished:

webView.loadUrl("javascript:openChannel()")

or you remove that function entirely and leave part of the script in the main body (see below).

  1. The functions

        onSomething = function() {
            ChannelListener.onSomething();
        };
    

need to be terminated using a semicolon ; as discussed here var functionName = function() {} vs function functionName() {} and Android is picky about this. If you don't properly close it, the WebView will do NOTHING!

Here is the full working code using option 2:

<html>
    <head>
        <script src='{{ channelurl }}jsapi'></script>
    </head>
    <body>
        <script type="text/javascript">

            onOpen = function() {
                ChannelListener.onOpen();
            };
            onMessage = function(message) {
                ChannelListener.onMessage(message.data);
            };
            onError = function(error) {
                ChannelListener.onError(error.code, error.description);
            };
            onClose = function() {
                ChannelListener.onClose();
            };

            var token = '{{ token }}';
            var channel = new goog.appengine.Channel(token);

            var handler = {
                'onopen': onOpen,
                'onmessage': onMessage,
                'onerror': onError,
                'onclose': onClose
            };

            var socket = channel.open(handler);

            socket.onopen = onOpen;
            socket.onmessage = onMessage;
            socket.onerror = onError;
            socket.onclose = onClose;

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

Now only one problem remains, but this is a different story: onMessage(message) does not pass the message argument across the handler, probably because it is executed in a different scope. I posted this problam as a new question here: JavaScript Argument not passed across handler

--

I have found the problem why I didn't receive the message from the server and edited the code above. To receive the message, you must pass message.data instead of just the message to Java to receive the string message the server has sent.

--

I blindly adjusted the onError function as well according to Google's specs as defined here https://cloud.google.com/appengine/docs/java/channel/javascript, but as Google hasn't implemented error handling in their sample code here http://code.google.com/p/channel-tac-toe/source/browse/trunk/index.html I cannot say for sure that it works like that, but I assume it does. If you come across this and can confirm that it works well, please comment.

Community
  • 1
  • 1
Oliver Hausler
  • 4,501
  • 4
  • 30
  • 62