diff --git a/framework/src/org/apache/cordova/CallbackServer.java b/framework/src/org/apache/cordova/CallbackServer.java index b8075b58..d0296a30 100755 --- a/framework/src/org/apache/cordova/CallbackServer.java +++ b/framework/src/org/apache/cordova/CallbackServer.java @@ -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 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(); } /** @@ -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(); // 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(); } } diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java index cdfc9d1f..7feee199 100755 --- a/framework/src/org/apache/cordova/CordovaChromeClient.java +++ b/framework/src/org/apache/cordova/CordovaChromeClient.java @@ -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); } diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 85c3cd82..131f754f 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -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); } /** diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index 6e4ff7f3..037f2003 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -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); diff --git a/framework/src/org/apache/cordova/DroidGap.java b/framework/src/org/apache/cordova/DroidGap.java index 68dbe08b..03ca2ad1 100755 --- a/framework/src/org/apache/cordova/DroidGap.java +++ b/framework/src/org/apache/cordova/DroidGap.java @@ -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); } } diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java new file mode 100755 index 00000000..037cc9c8 --- /dev/null +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -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 queue = new LinkedList(); + + /** + * 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); + } + } + } + +}