From 1fa63300aa9783e4664d43c745df7b9013653602 Mon Sep 17 00:00:00 2001 From: Max Woghiren Date: Wed, 27 Mar 2013 12:09:17 -0400 Subject: [PATCH 1/5] [CB-2666] Added check for null arguments. If null arguments are received, send an error and an explanation. --- framework/src/org/apache/cordova/ExposedJsApi.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/framework/src/org/apache/cordova/ExposedJsApi.java b/framework/src/org/apache/cordova/ExposedJsApi.java index 48e71021..7702d350 100755 --- a/framework/src/org/apache/cordova/ExposedJsApi.java +++ b/framework/src/org/apache/cordova/ExposedJsApi.java @@ -20,6 +20,7 @@ package org.apache.cordova; import android.webkit.JavascriptInterface; import org.apache.cordova.api.PluginManager; +import org.apache.cordova.api.PluginResult; import org.json.JSONException; /** @@ -39,6 +40,12 @@ import org.json.JSONException; @JavascriptInterface public String exec(String service, String action, String callbackId, String arguments) throws JSONException { + // If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666. + // We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string. + if (arguments == null) { + return "@Null arguments."; + } + jsMessageQueue.setPaused(true); try { boolean wasSync = pluginManager.exec(service, action, callbackId, arguments); From 1782111d45caf74accd5981c07847400b8735aa2 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 28 Mar 2013 11:49:05 -0400 Subject: [PATCH 2/5] [CB-2654] Delay executeScript/insertCSS callback until resources have loaded; pass JS results to callback --- .../src/org/apache/cordova/InAppBrowser.java | 149 +++++++++++++++--- 1 file changed, 131 insertions(+), 18 deletions(-) diff --git a/framework/src/org/apache/cordova/InAppBrowser.java b/framework/src/org/apache/cordova/InAppBrowser.java index 0d5d4965..47f987ca 100644 --- a/framework/src/org/apache/cordova/InAppBrowser.java +++ b/framework/src/org/apache/cordova/InAppBrowser.java @@ -51,6 +51,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.webkit.WebChromeClient; import android.webkit.GeolocationPermissions.Callback; +import android.webkit.JsPromptResult; import android.webkit.WebSettings; import android.webkit.WebStorage; import android.webkit.WebView; @@ -94,10 +95,10 @@ public class InAppBrowser extends CordovaPlugin { public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { PluginResult.Status status = PluginResult.Status.OK; String result = ""; - this.callbackContext = callbackContext; try { if (action.equals("open")) { + this.callbackContext = callbackContext; String url = args.getString(0); String target = args.optString(1); if (target == null || target.equals("") || target.equals(NULL)) { @@ -143,8 +144,12 @@ public class InAppBrowser extends CordovaPlugin { Log.d(LOG_TAG, "in blank"); result = this.showWebPage(url, features); } + PluginResult pluginResult = new PluginResult(status, result); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); } else if (action.equals("close")) { + this.callbackContext = callbackContext; closeDialog(); PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); @@ -152,32 +157,82 @@ public class InAppBrowser extends CordovaPlugin { this.callbackContext.sendPluginResult(pluginResult); } else if (action.equals("injectScriptCode")) { - String source = args.getString(0); - - org.json.JSONArray jsonEsc = new org.json.JSONArray(); - jsonEsc.put(source); - String jsonRepr = jsonEsc.toString(); - String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1); - String scriptEnclosure = "(function(d){var c=d.createElement('script');c.type='text/javascript';c.innerText=" - + jsonSourceString - + ";d.getElementsByTagName('head')[0].appendChild(c);})(document)"; - this.inAppWebView.loadUrl("javascript:" + scriptEnclosure); - - PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); - this.callbackContext.sendPluginResult(pluginResult); + String jsWrapper = null; + if (args.getBoolean(1)) { + jsWrapper = String.format("prompt(JSON.stringify([eval(%%s)]), 'gap-iab://%s')", callbackContext.getCallbackId()); + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectScriptFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('script'); c.src = %%s; c.onload = function() { prompt('', 'gap-iab://%s'); }; d.body.appendChild(c); })(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleCode")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('style'); c.innerHTML = %%s; d.body.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('style'); c.innerHTML = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%s; d.head.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %s; d.head.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); } else { status = PluginResult.Status.INVALID_ACTION; + PluginResult pluginResult = new PluginResult(status, result); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); } - PluginResult pluginResult = new PluginResult(status, result); - pluginResult.setKeepCallback(true); - this.callbackContext.sendPluginResult(pluginResult); } catch (JSONException e) { this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); } return true; } + /** + * Inject an object (script or style) into the InAppBrowser WebView. + * + * This is a helper method for the inject{Script|Style}{Code|File} API calls, which + * provides a consistent method for injecting JavaScript code into the document. + * + * If a wrapper string is supplied, then the source string will be JSON-encoded (adding + * quotes) and wrapped using string formatting. (The wrapper string should have a single + * '%s' marker) + * + * @param source The source object (filename or script/style text) to inject into + * the document. + * @param jsWrapper A JavaScript string to wrap the source string in, so that the object + * is properly injected, or null if the source string is JavaScript text + * which should be executed directly. + */ + private void injectDeferredObject(String source, String jsWrapper) { + String scriptToInject; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } else { + scriptToInject = source; + } + // This action will have the side-effect of blurring the currently focused element + this.inAppWebView.loadUrl("javascript:" + scriptToInject); + } + /** * Put the list of features into a hash map * @@ -444,7 +499,7 @@ public class InAppBrowser extends CordovaPlugin { // WebView inAppWebView = new WebView(cordova.getActivity()); inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - inAppWebView.setWebChromeClient(new InAppChromeClient()); + inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView)); WebViewClient client = new InAppBrowserClient(thatWebView, edittext); inAppWebView.setWebViewClient(client); WebSettings settings = inAppWebView.getSettings(); @@ -527,8 +582,15 @@ public class InAppBrowser extends CordovaPlugin { result.setKeepCallback(keepCallback); this.callbackContext.sendPluginResult(result); } + public class InAppChromeClient extends WebChromeClient { + private CordovaWebView webView; + + public InAppChromeClient(CordovaWebView webView) { + super(); + this.webView = webView; + } /** * Handle database quota exceeded notification. * @@ -571,6 +633,57 @@ public class InAppBrowser extends CordovaPlugin { super.onGeolocationPermissionsShowPrompt(origin, callback); callback.invoke(origin, true, false); } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * The prompt bridge provided for the InAppBrowser is capable of executing any + * oustanding callback belonging to the InAppBrowser plugin. Care has been + * taken that other callbacks cannot be triggered, and that no other code + * execution is possible. + * + * To trigger the bridge, the prompt default value should be of the form: + * + * gap-iab:// + * + * where is the string id of the callback to trigger (something + * like "InAppBrowser0123456789") + * + * If present, the prompt message is expected to be a JSON-encoded value to + * pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid. + * + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + */ + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + // See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute. + if (defaultValue != null && defaultValue.startsWith("gap-iab://")) { + PluginResult scriptResult; + String scriptCallbackId = defaultValue.substring(10); + if (scriptCallbackId.startsWith("InAppBrowser")) { + if(message == null || message.length() == 0) { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray()); + } else { + try { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message)); + } catch(JSONException e) { + scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage()); + } + } + this.webView.sendPluginResult(scriptResult, scriptCallbackId); + result.confirm(""); + return true; + } + } + return false; + } + } /** From ba314246047501c36916604facb5e097572771e4 Mon Sep 17 00:00:00 2001 From: Steren Date: Tue, 2 Apr 2013 02:02:45 +0200 Subject: [PATCH 3/5] Keep the splashscreen image ratio instead of streatching it. An ImageView is used to be able to use ScaleType.CENTER_CROP, which is similar to the background-size:cover CSS property --- framework/src/org/apache/cordova/DroidGap.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/framework/src/org/apache/cordova/DroidGap.java b/framework/src/org/apache/cordova/DroidGap.java index d4296cbe..a9efc93e 100755 --- a/framework/src/org/apache/cordova/DroidGap.java +++ b/framework/src/org/apache/cordova/DroidGap.java @@ -51,6 +51,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.widget.ImageView; import android.webkit.ValueCallback; import android.webkit.WebViewClient; import android.widget.LinearLayout; @@ -1031,7 +1032,13 @@ public class DroidGap extends Activity implements CordovaInterface { root.setBackgroundColor(that.getIntegerProperty("backgroundColor", Color.BLACK)); root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT, 0.0F)); - root.setBackgroundResource(that.splashscreen); + // We want the splashscreen to keep its ratio, + // for this we need to use an ImageView and not simply the background of the LinearLayout + ImageView splashscreenView = new ImageView(that.getActivity()); + splashscreenView.setImageResource(that.splashscreen); + splashscreenView.setScaleType(ImageView.ScaleType.CENTER_CROP); // similar to the background-size:cover CSS property + splashscreenView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + root.addView(splashscreenView); // Create and show the dialog splashDialog = new Dialog(that, android.R.style.Theme_Translucent_NoTitleBar); From 5ff900f7ecfb0d2ee270697c66f64979f0008700 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 4 Apr 2013 16:45:39 -0400 Subject: [PATCH 4/5] Fixup for CB-2654. --- .../src/org/apache/cordova/InAppBrowser.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/framework/src/org/apache/cordova/InAppBrowser.java b/framework/src/org/apache/cordova/InAppBrowser.java index 47f987ca..8e78baa2 100644 --- a/framework/src/org/apache/cordova/InAppBrowser.java +++ b/framework/src/org/apache/cordova/InAppBrowser.java @@ -93,9 +93,6 @@ public class InAppBrowser extends CordovaPlugin { * @return A PluginResult object with a status and message. */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - PluginResult.Status status = PluginResult.Status.OK; - String result = ""; - try { if (action.equals("open")) { this.callbackContext = callbackContext; @@ -109,6 +106,7 @@ public class InAppBrowser extends CordovaPlugin { Log.d(LOG_TAG, "target = " + target); url = updateUrl(url); + String result = ""; // SELF if (SELF.equals(target)) { @@ -144,17 +142,14 @@ public class InAppBrowser extends CordovaPlugin { Log.d(LOG_TAG, "in blank"); result = this.showWebPage(url, features); } - PluginResult pluginResult = new PluginResult(status, result); + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); pluginResult.setKeepCallback(true); this.callbackContext.sendPluginResult(pluginResult); } else if (action.equals("close")) { - this.callbackContext = callbackContext; closeDialog(); - - PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); - pluginResult.setKeepCallback(false); - this.callbackContext.sendPluginResult(pluginResult); + this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); } else if (action.equals("injectScriptCode")) { String jsWrapper = null; @@ -191,10 +186,7 @@ public class InAppBrowser extends CordovaPlugin { injectDeferredObject(args.getString(0), jsWrapper); } else { - status = PluginResult.Status.INVALID_ACTION; - PluginResult pluginResult = new PluginResult(status, result); - pluginResult.setKeepCallback(true); - this.callbackContext.sendPluginResult(pluginResult); + return false; } } catch (JSONException e) { this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); From 778b784eb6f78a467d010dbfb2ecd2ee0b3ac3fc Mon Sep 17 00:00:00 2001 From: HUANG Menghuai Date: Mon, 1 Apr 2013 12:40:51 +0800 Subject: [PATCH 5/5] [CB-2908] Fix the DroidGap activity Lifecycle broken issue Attempting to invoke the Activity's finish() onDestroy breaks an Activity's lifecycle flag. OnDestroy can be called by the system, for instance, on restarting an Activity, it's definitely different from a normal finish(). Finish() incorrectly in onDestroy results in another DroidGap derived activity is started, while the original one is not yet onDestroy. This issue could be found when the system is trying to restart the activity upon, for instance, receiving immediately successive device Config changes. --- framework/src/org/apache/cordova/DroidGap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/org/apache/cordova/DroidGap.java b/framework/src/org/apache/cordova/DroidGap.java index a9efc93e..bb895e25 100755 --- a/framework/src/org/apache/cordova/DroidGap.java +++ b/framework/src/org/apache/cordova/DroidGap.java @@ -718,7 +718,7 @@ public class DroidGap extends Activity implements CordovaInterface { appView.handleDestroy(); } else { - this.endActivity(); + this.activityState = ACTIVITY_EXITING; } }