Merge branch 'master' into next
diff --git a/lib/android/exec.js b/lib/android/exec.js
index 506fd3d..7a101c6 100644
--- a/lib/android/exec.js
+++ b/lib/android/exec.js
@@ -203,7 +203,7 @@
             } else {
                 payload = JSON.parse(message.slice(nextSpaceIdx + 1));
             }
-            cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
+            cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback);
         } else {
             console.log("processMessage failed: invalid message:" + message);
         }
diff --git a/lib/bada/plugin/bada/device.js b/lib/bada/plugin/bada/device.js
index 75a41e8..c2db705 100644
--- a/lib/bada/plugin/bada/device.js
+++ b/lib/bada/plugin/bada/device.js
@@ -22,9 +22,6 @@
 var channel = require('cordova/channel'),

     utils = require('cordova/utils');

 

-// Tell cordova channel to wait on the CordovaInfoReady event

-channel.waitForInitialization('onCordovaInfoReady');

-

 function Device() {

     this.platform = null;

     this.version = null;

diff --git a/lib/common/channel.js b/lib/common/channel.js
index 6e90603..38daedf 100644
--- a/lib/common/channel.js
+++ b/lib/common/channel.js
@@ -242,6 +242,10 @@
 // Event to indicate that the connection property has been set.
 channel.createSticky('onCordovaConnectionReady');
 
+// Event to indicate that all automatically loaded JS plugins are loaded and ready.
+// This is used in conjunction with the automatic plugin JS loading CLI prototype.
+channel.createSticky('onPluginsReady');
+
 // Event to indicate that Cordova is ready
 channel.createSticky('onDeviceReady');
 
diff --git a/lib/common/plugin/FileTransfer.js b/lib/common/plugin/FileTransfer.js
index 7e7af61..3a9fda2 100644
--- a/lib/common/plugin/FileTransfer.js
+++ b/lib/common/plugin/FileTransfer.js
@@ -79,7 +79,7 @@
     }
 
     var fail = errorCallback && function(e) {
-        var error = new FileTransferError(e.code, e.source, e.target, e.http_status);
+        var error = new FileTransferError(e.code, e.source, e.target, e.http_status, e.body);
         errorCallback(error);
     };
 
@@ -129,7 +129,7 @@
     };
 
     var fail = errorCallback && function(e) {
-        var error = new FileTransferError(e.code, e.source, e.target, e.http_status);
+        var error = new FileTransferError(e.code, e.source, e.target, e.http_status, e.body);
         errorCallback(error);
     };
 
diff --git a/lib/common/plugin/echo.js b/lib/common/plugin/echo.js
index 87a495f..76fe3f0 100644
--- a/lib/common/plugin/echo.js
+++ b/lib/common/plugin/echo.js
@@ -19,7 +19,8 @@
  *
 */
 
-var exec = require('cordova/exec');
+var exec = require('cordova/exec'),
+    utils = require('cordova/utils');
 
 /**
  * Sends the given message through exec() to the Echo plugin, which sends it back to the successCallback.
@@ -29,10 +30,24 @@
  * @param forceAsync  Whether to force an async return value (for testing native->js bridge).
  */
 module.exports = function(successCallback, errorCallback, message, forceAsync) {
-    var action = forceAsync ? 'echoAsync' : 'echo';
-    if (!forceAsync && message.constructor == ArrayBuffer) {
-        action = 'echoArrayBuffer';
+    var action = 'echo';
+    var messageIsMultipart = (utils.typeName(message) == "Array");
+    var args = messageIsMultipart ? message : [message];
+
+    if (utils.typeName(message) == 'ArrayBuffer') {
+        if (forceAsync) {
+            console.warn('Cannot echo ArrayBuffer with forced async, falling back to sync.');
+        }
+        action += 'ArrayBuffer';
+    } else if (messageIsMultipart) {
+        if (forceAsync) {
+            console.warn('Cannot echo MultiPart Array with forced async, falling back to sync.');
+        }
+        action += 'MultiPart';
+    } else if (forceAsync) {
+        action += 'Async';
     }
-    exec(successCallback, errorCallback, "Echo", action, [message]);
+
+    exec(successCallback, errorCallback, "Echo", action, args);
 };
 
diff --git a/lib/cordova.js b/lib/cordova.js
index ef68b2b..a31c40e 100644
--- a/lib/cordova.js
+++ b/lib/cordova.js
@@ -189,7 +189,7 @@
      */
     callbackSuccess: function(callbackId, args) {
         try {
-            cordova.callbackFromNative(callbackId, true, args.status, args.message, args.keepCallback);
+            cordova.callbackFromNative(callbackId, true, args.status, [args.message], args.keepCallback);
         } catch (e) {
             console.log("Error in error callback: " + callbackId + " = "+e);
         }
@@ -202,7 +202,7 @@
         // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative.
         // Derive success from status.
         try {
-            cordova.callbackFromNative(callbackId, false, args.status, args.message, args.keepCallback);
+            cordova.callbackFromNative(callbackId, false, args.status, [args.message], args.keepCallback);
         } catch (e) {
             console.log("Error in error callback: " + callbackId + " = "+e);
         }
@@ -211,13 +211,13 @@
     /**
      * Called by native code when returning the result from an action.
      */
-    callbackFromNative: function(callbackId, success, status, message, keepCallback) {
+    callbackFromNative: function(callbackId, success, status, args, keepCallback) {
         var callback = cordova.callbacks[callbackId];
         if (callback) {
             if (success && status == cordova.callbackStatus.OK) {
-                callback.success && callback.success(message);
+                callback.success && callback.success.apply(null, args);
             } else if (!success) {
-                callback.fail && callback.fail(message);
+                callback.fail && callback.fail.apply(null, args);
             }
 
             // Clear callback if not expecting any more results
diff --git a/lib/ios/exec.js b/lib/ios/exec.js
index 58e73d2..5e237d9 100644
--- a/lib/ios/exec.js
+++ b/lib/ios/exec.js
@@ -68,6 +68,7 @@
     if (!args || utils.typeName(args) != 'Array') {
        return args;
     }
+    var ret = [];
     var encodeArrayBufferAs8bitString = function(ab) {
         return String.fromCharCode.apply(null, new Uint8Array(ab));
     };
@@ -76,17 +77,19 @@
     };
     args.forEach(function(arg, i) {
         if (utils.typeName(arg) == 'ArrayBuffer') {
-            args[i] = {
+            ret.push({
                 'CDVType': 'ArrayBuffer',
                 'data': encodeArrayBufferAsBase64(arg)
-            };
+            });
+        } else {
+            ret.push(arg);
         }
     });
-    return args;
+    return ret;
 }
 
-function massagePayloadNativeToJs(payload) {
-    if (payload && payload.hasOwnProperty('CDVType') && payload.CDVType == 'ArrayBuffer') {
+function massageMessageNativeToJs(message) {
+    if (message.CDVType == 'ArrayBuffer') {
         var stringToArrayBuffer = function(str) {
             var ret = new Uint8Array(str.length);
             for (var i = 0; i < str.length; i++) {
@@ -97,9 +100,23 @@
         var base64ToArrayBuffer = function(b64) {
             return stringToArrayBuffer(atob(b64));
         };
-        payload = base64ToArrayBuffer(payload.data);
+        message = base64ToArrayBuffer(message.data);
     }
-    return payload;
+    return message;
+}
+
+function convertMessageToArgsNativeToJs(message) {
+    var args = [];
+    if (!message || !message.hasOwnProperty('CDVType')) {
+        args.push(message);
+    } else if (message.CDVType == 'MultiPart') {
+        message.messages.forEach(function(e) {
+            args.push(massageMessageNativeToJs(e));
+        });
+    } else {
+        args.push(massageMessageNativeToJs(message));
+    }
+    return args;
 }
 
 function iOSExec() {
@@ -206,11 +223,11 @@
     return json;
 };
 
-iOSExec.nativeCallback = function(callbackId, status, payload, keepCallback) {
+iOSExec.nativeCallback = function(callbackId, status, message, keepCallback) {
     return iOSExec.nativeEvalAndFetch(function() {
         var success = status === 0 || status === 1;
-        payload = massagePayloadNativeToJs(payload);
-        cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
+        var args = convertMessageToArgsNativeToJs(message);
+        cordova.callbackFromNative(callbackId, success, status, args, keepCallback);
     });
 };
 
diff --git a/lib/scripts/bootstrap.js b/lib/scripts/bootstrap.js
index 45c70aa..6213165 100644
--- a/lib/scripts/bootstrap.js
+++ b/lib/scripts/bootstrap.js
@@ -22,44 +22,26 @@
 (function (context) {
     // Replace navigator before any modules are required(), to ensure it happens as soon as possible.
     // We replace it so that properties that can't be clobbered can instead be overridden.
-    if (context.navigator) {
+    function replaceNavigator(origNavigator) {
         var CordovaNavigator = function() {};
-        CordovaNavigator.prototype = context.navigator;
-        context.navigator = new CordovaNavigator();
+        CordovaNavigator.prototype = origNavigator;
+        var newNavigator = new CordovaNavigator();
+        // This work-around really only applies to new APIs that are newer than Function.bind.
+        // Without it, APIs such as getGamepads() break.
+        if (CordovaNavigator.bind) {
+            for (var key in origNavigator) {
+                if (typeof origNavigator[key] == 'function') {
+                    newNavigator[key] = origNavigator[key].bind(origNavigator);
+                }
+            }
+        }
+        return newNavigator;
+    }
+    if (context.navigator) {
+        context.navigator = replaceNavigator(context.navigator);
     }
 
-    var channel = require("cordova/channel"),
-        _self = {
-            boot: function () {
-                /**
-                 * Create all cordova objects once page has fully loaded and native side is ready.
-                 */
-                channel.join(function() {
-                    var builder = require('cordova/builder'),
-                        platform = require('cordova/platform');
-
-                    builder.buildIntoButDoNotClobber(platform.defaults, context);
-                    builder.buildIntoAndClobber(platform.clobbers, context);
-                    builder.buildIntoAndMerge(platform.merges, context);
-
-                    // Call the platform-specific initialization
-                    platform.initialize();
-
-                    // Fire event to notify that all objects are created
-                    channel.onCordovaReady.fire();
-
-                    // Fire onDeviceReady event once all constructors have run and
-                    // cordova info has been received from native side.
-                    channel.join(function() {
-                        require('cordova').fireDocumentEvent('deviceready');
-                    }, channel.deviceReadyChannelsArray);
-
-                }, [ channel.onDOMContentLoaded, channel.onNativeReady ]);
-            }
-        };
-
-    // boot up once native side is ready
-    channel.onNativeReady.subscribe(_self.boot);
+    var channel = require("cordova/channel");
 
     // _nativeReady is global variable that the native side can set
     // to signify that the native code is ready. It is a global since
@@ -68,4 +50,38 @@
         channel.onNativeReady.fire();
     }
 
+    /**
+     * Create all cordova objects once page has fully loaded and native side is ready.
+     */
+    var joinEvents = [ channel.onDOMContentLoaded, channel.onNativeReady ];
+
+    // If this property is set to something truthy, join on onPluginsReady too.
+    // This property is set by the automatic JS installation prototype in cordova-cli,
+    // and will be removed when the prototype either becomes mainline or is dropped.
+    if (window.__onPluginsLoadedHack) {
+        joinEvents.push(channel.onPluginsReady);
+    }
+
+    channel.join(function() {
+        var builder = require('cordova/builder'),
+            platform = require('cordova/platform');
+
+        builder.buildIntoButDoNotClobber(platform.defaults, context);
+        builder.buildIntoAndClobber(platform.clobbers, context);
+        builder.buildIntoAndMerge(platform.merges, context);
+
+        // Call the platform-specific initialization
+        platform.initialize();
+
+        // Fire event to notify that all objects are created
+        channel.onCordovaReady.fire();
+
+        // Fire onDeviceReady event once all constructors have run and
+        // cordova info has been received from native side.
+        channel.join(function() {
+            require('cordova').fireDocumentEvent('deviceready');
+        }, channel.deviceReadyChannelsArray);
+
+    }, joinEvents);
+
 }(window));
diff --git a/test/android/test.exec.js b/test/android/test.exec.js
index 3363e45..3e185d9 100644
--- a/test/android/test.exec.js
+++ b/test/android/test.exec.js
@@ -107,50 +107,50 @@
         it('should handle payloads of false', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', 'f');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, false, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [false], true);
         });
         it('should handle payloads of true', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', 't');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, true, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [true], true);
         });
         it('should handle payloads of null', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', 'N');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, null, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [null], true);
         });
         it('should handle payloads of numbers', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', 'n-3.3');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, -3.3, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [-3.3], true);
         });
         it('should handle payloads of strings', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', 'sHello world');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, 'Hello world', true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, ['Hello world'], true);
         });
         it('should handle payloads of JSON objects', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', '{"a":1}');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, {a:1}, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [{a:1}], true);
         });
         it('should handle payloads of JSON arrays', function() {
             var messages = createCallbackMessage(true, true, 1, 'id', '[1]');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [1], true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [[1]], true);
         });
         it('should handle other callback opts', function() {
             var messages = createCallbackMessage(false, false, 3, 'id', 'sfoo');
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, 'foo', false);
+            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, ['foo'], false);
         });
         it('should handle multiple messages', function() {
             var message1 = createCallbackMessage(false, false, 3, 'id', 'sfoo');
             var message2 = createCallbackMessage(true, true, 1, 'id', 'f');
             var messages = message1 + message2;
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, 'foo', false);
-            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, false, true);
+            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, ['foo'], false);
+            expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [false], true);
         });
         it('should poll for more messages when hitting an *', function() {
             var message1 = createCallbackMessage(false, false, 3, 'id', 'sfoo');
@@ -161,10 +161,10 @@
             });
             var messages = message1 + '*';
             exec.processMessages(messages);
-            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, 'foo', false);
+            expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, ['foo'], false);
             waitsFor(function() { return nativeApi.retrieveJsMessages.wasCalled }, 500);
             runs(function() {
-                expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, false, true);
+                expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [false], true);
             });
         });
         it('should call callbacks in order when one callback enqueues another.', function() {
@@ -179,9 +179,9 @@
             });
             exec.processMessages(message1 + message2);
             expect(callbackSpy.argsForCall.length).toEqual(3);
-            expect(callbackSpy.argsForCall[0]).toEqual(['id', false, 3, 'call1', false]);
-            expect(callbackSpy.argsForCall[1]).toEqual(['id', false, 3, 'call2', false]);
-            expect(callbackSpy.argsForCall[2]).toEqual(['id', false, 3, 'call3', false]);
+            expect(callbackSpy.argsForCall[0]).toEqual(['id', false, 3, ['call1'], false]);
+            expect(callbackSpy.argsForCall[1]).toEqual(['id', false, 3, ['call2'], false]);
+            expect(callbackSpy.argsForCall[2]).toEqual(['id', false, 3, ['call3'], false]);
         });
     });
 });