Refactor Native->JS messaging logic into its own class.

This will make it easy to add more modes.
This also adds logic to set the move via a prompt() from JS.
This commit is contained in:
Andrew Grieve 2012-08-10 12:55:29 -04:00
parent a9a5284a67
commit 5e3e9ddb8e
6 changed files with 201 additions and 82 deletions

View File

@ -59,8 +59,9 @@ public class CallbackServer implements Runnable {
private ServerSocket waitSocket;
/**
* The list of JavaScript statements to be sent to JavaScript.
* This can be null when there are no messages available.
*/
private LinkedList<String> javascript;
private NativeToJsMessageQueue jsMessageQueue;
/**
* The port to listen on.
@ -77,10 +78,6 @@ public class CallbackServer implements Runnable {
*/
private boolean active;
/**
* Indicates that the JavaScript statements list is empty
*/
private boolean empty;
/**
* Indicates that polling should be used instead of XHR.
@ -98,9 +95,7 @@ public class CallbackServer implements Runnable {
public CallbackServer() {
//Log.d(LOG_TAG, "CallbackServer()");
this.active = false;
this.empty = true;
this.port = 0;
this.javascript = new LinkedList<String>();
}
/**
@ -113,10 +108,8 @@ public class CallbackServer implements Runnable {
*/
public void init(String url) {
//System.out.println("CallbackServer.start("+url+")");
this.active = false;
this.empty = true;
this.stopServer();
this.port = 0;
this.javascript = new LinkedList<String>();
// Determine if XHR or polling is to be used
if ((url != null) && !url.startsWith("file://")) {
@ -133,16 +126,6 @@ public class CallbackServer implements Runnable {
}
}
/**
* Re-init when loading a new HTML page into webview.
*
* @param url The URL of the Cordova app being loaded
*/
public void reinit(String url) {
this.stopServer();
this.init(url);
}
/**
* Return if polling is being used instead of XHR.
* @return
@ -224,11 +207,19 @@ public class CallbackServer implements Runnable {
// Must have security token
if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
//Log.d(LOG_TAG, "CallbackServer -- Processing GET request");
String js = null;
// Wait until there is some data to send, or send empty data every 10 sec
// to prevent XHR timeout on the client
synchronized (this) {
while (this.empty) {
while (this.active) {
if (jsMessageQueue != null) {
// TODO(agrieve): Should this use popAll() instead?
js = jsMessageQueue.pop();
if (js != null) {
break;
}
}
try {
this.wait(10000); // prevent timeout from happening
//Log.d(LOG_TAG, "CallbackServer>>> break <<<");
@ -242,17 +233,14 @@ public class CallbackServer implements Runnable {
if (this.active) {
// If no data, then send 404 back to client before it times out
if (this.empty) {
if (js == null) {
//Log.d(LOG_TAG, "CallbackServer -- sending data 0");
response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
}
else {
//Log.d(LOG_TAG, "CallbackServer -- sending item");
response = "HTTP/1.1 200 OK\r\n\r\n";
String js = this.getJavascript();
if (js != null) {
response += encode(js, "UTF-8");
}
response += encode(js, "UTF-8");
}
}
else {
@ -305,46 +293,10 @@ public class CallbackServer implements Runnable {
public void destroy() {
this.stopServer();
}
/**
* Get the number of JavaScript statements.
*
* @return int
*/
public int getSize() {
public void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue) {
synchronized (this) {
int size = this.javascript.size();
return size;
}
}
/**
* Get the next JavaScript statement and remove from list.
*
* @return String
*/
public String getJavascript() {
synchronized (this) {
if (this.javascript.size() == 0) {
return null;
}
String statement = this.javascript.remove(0);
if (this.javascript.size() == 0) {
this.empty = true;
}
return statement;
}
}
/**
* Add a JavaScript statement to the list.
*
* @param statement
*/
public void sendJavascript(String statement) {
synchronized (this) {
this.javascript.add(statement);
this.empty = false;
this.jsMessageQueue = queue;
this.notify();
}
}

View File

@ -23,12 +23,9 @@ import org.apache.cordova.api.LOG;
import org.json.JSONArray;
import org.json.JSONException;
//import android.app.Activity;
import android.app.AlertDialog;
//import android.content.Context;
import android.content.DialogInterface;
import android.view.KeyEvent;
//import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
@ -211,9 +208,16 @@ public class CordovaChromeClient extends WebChromeClient {
}
}
// Sets the native->JS bridge mode.
else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) {
this.appView.jsMessageQueue.setBridgeMode(Integer.parseInt(message));
result.confirm("");
}
// Polling for JavaScript messages
else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
String r = this.appView.callbackServer.getJavascript();
// TODO(agrieve): Use popAll() here.
String r = this.appView.jsMessageQueue.pop();
result.confirm(r);
}

View File

@ -88,6 +88,8 @@ public class CordovaWebView extends WebView {
private boolean handleButton = false;
NativeToJsMessageQueue jsMessageQueue;
/**
* Constructor.
*
@ -190,14 +192,14 @@ public class CordovaWebView extends WebView {
}
}
/**
* Initialize webview.
*/
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private void setup() {
jsMessageQueue = new NativeToJsMessageQueue(this);
this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false);
this.requestFocusFromTouch();
@ -456,7 +458,7 @@ public class CordovaWebView extends WebView {
*
* @param url
*/
private void loadUrlNow(String url) {
void loadUrlNow(String url) {
LOG.d(TAG, ">>> loadUrlNow()");
super.loadUrl(url);
}
@ -495,9 +497,7 @@ public class CordovaWebView extends WebView {
* @param message
*/
public void sendJavascript(String statement) {
if (this.callbackServer != null) {
this.callbackServer.sendJavascript(statement);
}
this.jsMessageQueue.add(statement);
}
/**

View File

@ -230,14 +230,14 @@ public class CordovaWebViewClient extends WebViewClient {
this.doClearHistory = true;
}
// Create callback server and plugin manager
// Flush stale messages.
this.appView.jsMessageQueue.reset();
// Create callback server
if (this.appView.callbackServer == null) {
this.appView.callbackServer = new CallbackServer();
this.appView.callbackServer.init(url);
}
else {
this.appView.callbackServer.reinit(url);
}
this.appView.callbackServer.init(url);
// Broadcast message that page has loaded
this.appView.postMessage("onPageStarted", url);

View File

@ -714,8 +714,8 @@ public class DroidGap extends Activity implements CordovaInterface {
* @param message
*/
public void sendJavascript(String statement) {
if (this.appView != null && this.appView.callbackServer != null) {
this.appView.callbackServer.sendJavascript(statement);
if (this.appView != null) {
this.appView.jsMessageQueue.add(statement);
}
}

View File

@ -0,0 +1,163 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova;
import java.util.LinkedList;
import android.util.Log;
/**
* Holds the list of messages to be sent to the WebView.
*/
public class NativeToJsMessageQueue {
private static final String LOG_TAG = "JsMessageQueue";
// This must match the default value in incubator-cordova-js/lib/android/exec.js
private static final int DEFAULT_BRIDGE_MODE = 1;
/**
* The list of JavaScript statements to be sent to JavaScript.
*/
private LinkedList<String> queue = new LinkedList<String>();
/**
* The index into registeredListeners to treat as active.
*/
private int activeListenerIndex;
/**
* The array of listeners that can be used to send messages to JS.
*/
private BridgeMode[] registeredListeners;
public NativeToJsMessageQueue(CordovaWebView webView) {
registeredListeners = new BridgeMode[2];
registeredListeners[0] = null;
registeredListeners[1] = new CallbackBridgeMode(webView);
reset();
// POLLING: 0,
// HANGING_GET: 1,
// LOAD_URL: 2,
// ONLINE_EVENT: 3,
// PRIVATE_API: 4
}
/**
* Changes the bridge mode.
*/
public void setBridgeMode(int value) {
if (value < 0 || value >= registeredListeners.length) {
Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value);
} else {
if (value != activeListenerIndex) {
Log.d(LOG_TAG, "Set native->JS mode to " + value);
synchronized (this) {
activeListenerIndex = value;
BridgeMode activeListener = registeredListeners[value];
if (!queue.isEmpty() && activeListener != null) {
activeListener.onNativeToJsMessageAvailable(this);
}
}
}
}
}
/**
* Clears all messages and resets to the default bridge mode.
*/
public void reset() {
synchronized (this) {
queue.clear();
setBridgeMode(DEFAULT_BRIDGE_MODE);
}
}
/**
* Removes and returns the last statement in the queue.
* Returns null if the queue is empty.
*/
public String pop() {
synchronized (this) {
if (queue.isEmpty()) {
return null;
}
return queue.remove(0);
}
}
/**
* Combines and returns all statements. Clears the queue.
* Returns null if the queue is empty.
*/
public String popAll() {
synchronized (this) {
int length = queue.size();
if (length == 0) {
return null;
}
StringBuffer sb = new StringBuffer();
// Wrap each statement in a try/finally so that if one throws it does
// not affect the next.
int i = 0;
for (String message : queue) {
if (++i == length) {
sb.append(message);
} else {
sb.append("try{")
.append(message)
.append("}finally{");
}
}
for ( i = 1; i < length; ++i) {
sb.append('}');
}
queue.clear();
return sb.toString();
}
}
/**
* Add a JavaScript statement to the list.
*/
public void add(String statement) {
synchronized (this) {
queue.add(statement);
if (registeredListeners[activeListenerIndex] != null) {
registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(this);
}
}
}
private interface BridgeMode {
void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue);
}
private static class CallbackBridgeMode implements BridgeMode {
private CordovaWebView webView;
public CallbackBridgeMode(CordovaWebView webView) {
this.webView = webView;
}
public void onNativeToJsMessageAvailable(NativeToJsMessageQueue queue) {
if (webView.callbackServer != null) {
webView.callbackServer.onNativeToJsMessageAvailable(queue);
}
}
}
}