Merge CB-7992 purplecabbage/cordova-js This closes #106
diff --git a/README.md b/README.md
index a372a56..72cb379 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,9 @@
 
 [![Build Status](https://travis-ci.org/apache/cordova-js.svg?branch=master)](https://travis-ci.org/apache/cordova-js)
 
+[![Build status](https://ci.appveyor.com/api/projects/status/github/apache/cordova-js?branch=master&svg=true)](https://ci.appveyor.com/project/Humbedooh/cordova-js/branch/master)
+
+
 A unified JavaScript layer for [Apache Cordova](http://cordova.apache.org/) projects.
 
 # Project Structure
diff --git a/src/browser/confighelper.js b/src/browser/confighelper.js
new file mode 100644
index 0000000..de19c8d
--- /dev/null
+++ b/src/browser/confighelper.js
@@ -0,0 +1,95 @@
+/*
+ *
+ * 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.
+ *
+*/
+
+var config;
+
+function Config(xhr) {
+    function loadPreferences(xhr) {
+       var parser = new DOMParser();
+       var doc = parser.parseFromString(xhr.responseText, "application/xml");
+
+       var preferences = doc.getElementsByTagName("preference");
+       return Array.prototype.slice.call(preferences);
+    }
+
+    this.xhr = xhr;
+    this.preferences = loadPreferences(this.xhr);
+}
+
+function readConfig(success, error) {
+    var xhr;
+
+    if(typeof config != 'undefined') {
+        success(config);
+    }
+
+    function fail(msg) {
+        console.error(msg);
+
+        if(error) {
+            error(msg);
+        }
+    }
+
+    var xhrStatusChangeHandler = function() {
+        if (xhr.readyState == 4) {
+            if (xhr.status == 200 || xhr.status == 304 || xhr.status === 0 /* file:// */) {
+                config = new Config(xhr);
+                success(config);
+            }
+            else {
+                fail('[Browser][cordova.js][xhrStatusChangeHandler] Could not XHR config.xml: ' + xhr.statusText);
+            }
+        }
+    };
+
+    if ("ActiveXObject" in window) {
+        // Needed for XHR-ing via file:// protocol in IE
+        xhr = new window.ActiveXObject("MSXML2.XMLHTTP");
+        xhr.onreadystatechange = xhrStatusChangeHandler;
+    } else {
+        xhr = new XMLHttpRequest();
+        xhr.addEventListener("load", xhrStatusChangeHandler);
+    }
+
+    try {
+        xhr.open("get", "config.xml", true);
+        xhr.send();
+    } catch(e) {
+        fail('[Browser][cordova.js][readConfig] Could not XHR config.xml: ' + JSON.stringify(e));
+    }
+}
+
+/**
+ * Reads a preference value from config.xml.
+ * Returns preference value or undefined if it does not exist.
+ * @param {String} preferenceName Preference name to read */
+Config.prototype.getPreferenceValue = function getPreferenceValue(preferenceName) {
+    var preferenceItem = this.preferences && this.preferences.filter(function(item) {
+        return item.attributes.name && item.attributes.name.value === preferenceName;
+    });
+
+    if(preferenceItem && preferenceItem[0] && preferenceItem[0].attributes && preferenceItem[0].attributes.value) {
+        return preferenceItem[0].attributes.value.value;
+    }
+};
+
+exports.readConfig = readConfig;
diff --git a/test/android/test.exec.js b/test/android/test.exec.js
index 6541f3c..a8e0b24 100644
--- a/test/android/test.exec.js
+++ b/test/android/test.exec.js
@@ -19,7 +19,7 @@
  *
 */
 
-describe('exec.processMessages', function () {
+describe('android exec.processMessages', function () {
     var cordova = require('cordova'),
         exec = require('cordova/android/exec'),
         nativeApiProvider = require('cordova/android/nativeapiprovider'),
@@ -127,13 +127,14 @@
         });
 
         function performExecAndReturn(messages) {
+
             nativeApi.exec.andCallFake(function(secret, service, action, callbackId, argsJson) {
                 return messages;
             });
 
-            var winSpy = jasmine.createSpy('win');
-
             exec(null, null, 'Service', 'action', []);
+            // note: sometimes we need to wait for multiple callbacks, this returns after one
+            // see 'should handle multiple messages' below
             waitsFor(function() { return callbackSpy.wasCalled }, 200);
         }
 
@@ -198,6 +199,12 @@
             var message2 = createCallbackMessage(true, true, 1, 'id', 'f');
             var messages = message1 + message2;
             performExecAndReturn(messages);
+
+            // need to wait for ALL the callbacks before we check our expects
+            waitsFor(function(){
+                return callbackSpy.calls.length > 1;
+            },200);
+
             runs(function() {
                 expect(callbackSpy).toHaveBeenCalledWith('id', false, 3, ['foo'], false);
                 expect(callbackSpy).toHaveBeenCalledWith('id', true, 1, [false], true);
@@ -228,6 +235,11 @@
                 }
             });
             performExecAndReturn(message1 + message2);
+            // need to wait for ALL the callbacks before we check our expects
+            waitsFor(function(){
+                return callbackSpy.calls.length > 2;
+            },200);
+
             runs(function() {
                 expect(callbackSpy.argsForCall.length).toEqual(3);
                 expect(callbackSpy.argsForCall[0]).toEqual(['id', false, 3, ['call1'], false]);