CB-7707 Added multipart PluginResult (close #125)

Corresponds to cordova-js commit: a1f866606b3

Conflicts:

	framework/assets/www/cordova.js
diff --git a/framework/assets/www/cordova.js b/framework/assets/www/cordova.js
index bdf1cf9..fc7cf88 100644
--- a/framework/assets/www/cordova.js
+++ b/framework/assets/www/cordova.js
@@ -262,11 +262,7 @@
      * Called by native code when returning successful result from an action.
      */
     callbackSuccess: function(callbackId, args) {
-        try {
-            cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
-        } catch (e) {
-            console.log("Error in success callback: " + callbackId + " = "+e);
-        }
+        cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
     },
 
     /**
@@ -275,30 +271,34 @@
     callbackError: function(callbackId, args) {
         // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative.
         // Derive success from status.
-        try {
-            cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
-        } catch (e) {
-            console.log("Error in error callback: " + callbackId + " = "+e);
-        }
+        cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
     },
 
     /**
      * Called by native code when returning the result from an action.
      */
-    callbackFromNative: function(callbackId, success, status, args, keepCallback) {
-        var callback = cordova.callbacks[callbackId];
-        if (callback) {
-            if (success && status == cordova.callbackStatus.OK) {
-                callback.success && callback.success.apply(null, args);
-            } else if (!success) {
-                callback.fail && callback.fail.apply(null, args);
-            }
+    callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
+        try {
+            var callback = cordova.callbacks[callbackId];
+            if (callback) {
+                if (isSuccess && status == cordova.callbackStatus.OK) {
+                    callback.success && callback.success.apply(null, args);
+                } else {
+                    callback.fail && callback.fail.apply(null, args);
+                }
 
-            // Clear callback if not expecting any more results
-            if (!keepCallback) {
-                delete cordova.callbacks[callbackId];
+                // Clear callback if not expecting any more results
+                if (!keepCallback) {
+                    delete cordova.callbacks[callbackId];
+                }
             }
         }
+        catch (err) {
+            var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err;
+            console && console.log && console.log(msg);
+            cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg });
+            throw err;
+        }
     },
     addConstructor: function(func) {
         channel.onCordovaReady.subscribe(function() {
@@ -1012,6 +1012,42 @@
     }
 };
 
+function buildPayload(payload, message) {
+    var payloadKind = message.charAt(0);
+    if (payloadKind == 's') {
+        payload.push(message.slice(1));
+    } else if (payloadKind == 't') {
+        payload.push(true);
+    } else if (payloadKind == 'f') {
+        payload.push(false);
+    } else if (payloadKind == 'N') {
+        payload.push(null);
+    } else if (payloadKind == 'n') {
+        payload.push(+message.slice(1));
+    } else if (payloadKind == 'A') {
+        var data = message.slice(1);
+        var bytes = window.atob(data);
+        var arraybuffer = new Uint8Array(bytes.length);
+        for (var i = 0; i < bytes.length; i++) {
+            arraybuffer[i] = bytes.charCodeAt(i);
+        }
+        payload.push(arraybuffer.buffer);
+    } else if (payloadKind == 'S') {
+        payload.push(window.atob(message.slice(1)));
+    } else if (payloadKind == 'M') {
+        var multipartMessages = message.slice(1);
+        while (multipartMessages !== "") {
+            var spaceIdx = multipartMessages.indexOf(' ');
+            var msgLen = +multipartMessages.slice(0, spaceIdx);
+            var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
+            multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
+            buildPayload(payload, multipartMessage);
+        }
+    } else {
+        payload.push(JSON.parse(message));
+    }
+}
+
 // Processes a single message, as encoded by NativeToJsMessageQueue.java.
 function processMessage(message) {
     try {
@@ -1025,32 +1061,10 @@
             var status = +message.slice(2, spaceIdx);
             var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
             var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
-            var payloadKind = message.charAt(nextSpaceIdx + 1);
-            var payload;
-            if (payloadKind == 's') {
-                payload = message.slice(nextSpaceIdx + 2);
-            } else if (payloadKind == 't') {
-                payload = true;
-            } else if (payloadKind == 'f') {
-                payload = false;
-            } else if (payloadKind == 'N') {
-                payload = null;
-            } else if (payloadKind == 'n') {
-                payload = +message.slice(nextSpaceIdx + 2);
-            } else if (payloadKind == 'A') {
-                var data = message.slice(nextSpaceIdx + 2);
-                var bytes = window.atob(data);
-                var arraybuffer = new Uint8Array(bytes.length);
-                for (var i = 0; i < bytes.length; i++) {
-                    arraybuffer[i] = bytes.charCodeAt(i);
-                }
-                payload = arraybuffer.buffer;
-            } else if (payloadKind == 'S') {
-                payload = window.atob(message.slice(nextSpaceIdx + 2));
-            } else {
-                payload = JSON.parse(message.slice(nextSpaceIdx + 1));
-            }
-            cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback);
+            var payloadMessage = message.slice(nextSpaceIdx + 1);
+            var payload = [];
+            buildPayload(payload, payloadMessage);
+            cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
         } else {
             console.log("processMessage failed: invalid message: " + JSON.stringify(message));
         }
@@ -1888,4 +1902,4 @@
 
 require('cordova/init');
 
-})();
\ No newline at end of file
+})();
diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java
index ebc93cb..6016793 100755
--- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java
+++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java
@@ -37,6 +37,7 @@
 
     // Set this to true to force plugin results to be encoding as
     // JS instead of the custom format (useful for benchmarking).
+    // Doesn't work for multipart messages.
     private static final boolean FORCE_ENCODE_USING_EVAL = false;
 
     // Disable sending back native->JS messages during an exec() when the active
@@ -419,53 +420,43 @@
             this.pluginResult = pluginResult;
         }
         
+        static int calculateEncodedLengthHelper(PluginResult pluginResult) {
+            switch (pluginResult.getMessageType()) {
+                case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t
+                case PluginResult.MESSAGE_TYPE_NULL: // N
+                    return 1;
+                case PluginResult.MESSAGE_TYPE_NUMBER: // n
+                    return 1 + pluginResult.getMessage().length();
+                case PluginResult.MESSAGE_TYPE_STRING: // s
+                    return 1 + pluginResult.getStrMessage().length();
+                case PluginResult.MESSAGE_TYPE_BINARYSTRING:
+                    return 1 + pluginResult.getMessage().length();
+                case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
+                    return 1 + pluginResult.getMessage().length();
+                case PluginResult.MESSAGE_TYPE_MULTIPART:
+                    int ret = 1;
+                    for (int i = 0; i < pluginResult.getMultipartMessagesSize(); i++) {
+                        int length = calculateEncodedLengthHelper(pluginResult.getMultipartMessage(i));
+                        int argLength = String.valueOf(length).length();
+                        ret += argLength + 1 + length;
+                    }
+                    return ret;
+                case PluginResult.MESSAGE_TYPE_JSON:
+                default:
+                    return pluginResult.getMessage().length();
+            }
+        }
+        
         int calculateEncodedLength() {
             if (pluginResult == null) {
                 return jsPayloadOrCallbackId.length() + 1;
             }
             int statusLen = String.valueOf(pluginResult.getStatus()).length();
             int ret = 2 + statusLen + 1 + jsPayloadOrCallbackId.length() + 1;
-            switch (pluginResult.getMessageType()) {
-                case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t
-                case PluginResult.MESSAGE_TYPE_NULL: // N
-                    ret += 1;
-                    break;
-                case PluginResult.MESSAGE_TYPE_NUMBER: // n
-                    ret += 1 + pluginResult.getMessage().length();
-                    break;
-                case PluginResult.MESSAGE_TYPE_STRING: // s
-                    ret += 1 + pluginResult.getStrMessage().length();
-                    break;
-                case PluginResult.MESSAGE_TYPE_BINARYSTRING:
-                    ret += 1 + pluginResult.getMessage().length();
-                    break;
-                case PluginResult.MESSAGE_TYPE_ARRAYBUFFER:
-                    ret += 1 + pluginResult.getMessage().length();
-                    break;
-                case PluginResult.MESSAGE_TYPE_JSON:
-                default:
-                    ret += pluginResult.getMessage().length();
+            return ret + calculateEncodedLengthHelper(pluginResult);
             }
-            return ret;
-        }
-        
-        void encodeAsMessage(StringBuilder sb) {
-            if (pluginResult == null) {
-                sb.append('J')
-                  .append(jsPayloadOrCallbackId);
-                return;
-            }
-            int status = pluginResult.getStatus();
-            boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
-            boolean resultOk = status == PluginResult.Status.OK.ordinal();
-            boolean keepCallback = pluginResult.getKeepCallback();
 
-            sb.append((noResult || resultOk) ? 'S' : 'F')
-              .append(keepCallback ? '1' : '0')
-              .append(status)
-              .append(' ')
-              .append(jsPayloadOrCallbackId)
-              .append(' ');
+        static void encodeAsMessageHelper(StringBuilder sb, PluginResult pluginResult) {
             switch (pluginResult.getMessageType()) {
                 case PluginResult.MESSAGE_TYPE_BOOLEAN:
                     sb.append(pluginResult.getMessage().charAt(0)); // t or f.
@@ -489,12 +480,42 @@
                     sb.append('A');
                     sb.append(pluginResult.getMessage());
                     break;
+                case PluginResult.MESSAGE_TYPE_MULTIPART:
+                    sb.append('M');
+                    for (int i = 0; i < pluginResult.getMultipartMessagesSize(); i++) {
+                        PluginResult multipartMessage = pluginResult.getMultipartMessage(i);
+                        sb.append(String.valueOf(calculateEncodedLengthHelper(multipartMessage)));
+                        sb.append(' ');
+                        encodeAsMessageHelper(sb, multipartMessage);
+                    }
+                    break;
                 case PluginResult.MESSAGE_TYPE_JSON:
                 default:
                     sb.append(pluginResult.getMessage()); // [ or {
             }
         }
         
+        void encodeAsMessage(StringBuilder sb) {
+            if (pluginResult == null) {
+                sb.append('J')
+                  .append(jsPayloadOrCallbackId);
+                return;
+            }
+            int status = pluginResult.getStatus();
+            boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
+            boolean resultOk = status == PluginResult.Status.OK.ordinal();
+            boolean keepCallback = pluginResult.getKeepCallback();
+
+            sb.append((noResult || resultOk) ? 'S' : 'F')
+              .append(keepCallback ? '1' : '0')
+              .append(status)
+              .append(' ')
+              .append(jsPayloadOrCallbackId)
+              .append(' ');
+
+            encodeAsMessageHelper(sb, pluginResult);
+        }
+
         void encodeAsJsMessage(StringBuilder sb) {
             if (pluginResult == null) {
                 sb.append(jsPayloadOrCallbackId);
diff --git a/framework/src/org/apache/cordova/PluginResult.java b/framework/src/org/apache/cordova/PluginResult.java
old mode 100755
new mode 100644
index 920cbc2..2b3ac72
--- a/framework/src/org/apache/cordova/PluginResult.java
+++ b/framework/src/org/apache/cordova/PluginResult.java
@@ -18,6 +18,8 @@
 */
 package org.apache.cordova;
 
+import java.util.List;
+
 import org.json.JSONArray;
 import org.json.JSONObject;
 
@@ -29,6 +31,7 @@
     private boolean keepCallback = false;
     private String strMessage;
     private String encodedMessage;
+    private List<PluginResult> multipartMessages;
 
     public PluginResult(Status status) {
         this(status, PluginResult.StatusMessages[status.ordinal()]);
@@ -80,6 +83,13 @@
         this.encodedMessage = Base64.encodeToString(data, Base64.NO_WRAP);
     }
     
+    // The keepCallback and status of multipartMessages are ignored.
+    public PluginResult(Status status, List<PluginResult> multipartMessages) {
+        this.status = status.ordinal();
+        this.messageType = MESSAGE_TYPE_MULTIPART;
+        this.multipartMessages = multipartMessages;
+    }
+
     public void setKeepCallback(boolean b) {
         this.keepCallback = b;
     }
@@ -99,6 +109,14 @@
         return encodedMessage;
     }
 
+    public int getMultipartMessagesSize() {
+        return multipartMessages.size();
+    }
+
+    public PluginResult getMultipartMessage(int index) {
+        return multipartMessages.get(index);
+    }
+
     /**
      * If messageType == MESSAGE_TYPE_STRING, then returns the message string.
      * Otherwise, returns null.
@@ -150,6 +168,7 @@
     // Use BINARYSTRING when your string may contain null characters.
     // This is required to work around a bug in the platform :(.
     public static final int MESSAGE_TYPE_BINARYSTRING = 7;
+    public static final int MESSAGE_TYPE_MULTIPART = 8;
 
     public static String[] StatusMessages = new String[] {
         "No result",