Bug 759637: Initial implementation of MobileConnection network selection APIs. r=philikon sr=sicking
authorMarshall Culpepper <marshall@mozilla.com>
Tue, 19 Jun 2012 15:52:06 -0700
changeset 97075 e5969cfec442567f800e1c1db8b69f6c13ec6a87
parent 97074 7fa9e53935cb6b4a5d3676a593cd997fd5811956
child 97076 73f68f4961b378ecc3bfd052da27ddd25854811b
push id22957
push usermh@glandium.org
push dateWed, 20 Jun 2012 08:54:19 +0000
treeherdermozilla-central@c3190d715044 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilikon, sicking
bugs759637
milestone16.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 759637: Initial implementation of MobileConnection network selection APIs. r=philikon sr=sicking
dom/network/interfaces/nsIDOMMobileConnection.idl
dom/network/interfaces/nsIMobileConnectionProvider.idl
dom/network/src/MobileConnection.cpp
dom/network/tests/marionette/test_mobile_networks.js
dom/system/gonk/RILContentHelper.js
dom/system/gonk/RadioInterfaceLayer.js
dom/system/gonk/ril_consts.js
dom/system/gonk/ril_worker.js
--- a/dom/network/interfaces/nsIDOMMobileConnection.idl
+++ b/dom/network/interfaces/nsIDOMMobileConnection.idl
@@ -26,27 +26,62 @@ interface nsIDOMMozMobileConnection : ns
   readonly attribute nsIDOMMozMobileConnectionInfo voice;
 
   /**
    * Information about the data connection.
    */
   readonly attribute nsIDOMMozMobileConnectionInfo data;
 
   /**
+   * The selection mode of the voice and data networks.
+   *
+   * Possible values: null (unknown), 'automatic', 'manual'
+   */
+  readonly attribute DOMString networkSelectionMode;
+
+  /**
    * Search for available networks.
    *
    * If successful, the request's onsuccess will be called, and the request's
    * result will be an array of nsIDOMMozMobileNetworkInfo.
    *
    * Otherwise, the request's onerror will be called, and the request's error
-   * will be either 'RadioNotAvailable', 'RequestNotSupported', or 'GenericFailure'.
+   * will be either 'RadioNotAvailable', 'RequestNotSupported',
+   * or 'GenericFailure'.
    */
   nsIDOMDOMRequest getNetworks();
 
   /**
+   * Manually selects the passed in network, overriding the radio's current
+   * selection.
+   *
+   * If successful, the request's onsuccess will be called.
+   * Note: If the network was actually changed by this request,
+   * the 'voicechange' and 'datachange' events will also be fired.
+   *
+   * Otherwise, the request's onerror will be called, and the request's error
+   * will be either 'RadioNotAvailable', 'RequestNotSupported',
+   * 'IllegalSIMorME', or 'GenericFailure'
+   */
+  nsIDOMDOMRequest selectNetwork(in nsIDOMMozMobileNetworkInfo network);
+
+  /**
+   * Tell the radio to automatically select a network.
+   *
+   * If successful, the request's onsuccess will be called.
+   * Note: If the network was actually changed by this request, the
+   * 'voicechange' and 'datachange' events will also be fired.
+   *
+   * Otherwise, the request's onerror will be called, and the request's error
+   * will be either 'RadioNotAvailable', 'RequestNotSupported',
+   * 'IllegalSIMorME', or 'GenericFailure'
+   */
+  nsIDOMDOMRequest selectNetworkAutomatically();
+
+  /**
    * Find out about the status of an ICC lock (e.g. the PIN lock).
    *
    * @param lockType
    *        Identifies the lock type, e.g. "pin" for the PIN lock.
    *
    * @return a DOM Request.
    *         The request's result will be an object containing
    *         information about the specified lock's status,
--- a/dom/network/interfaces/nsIMobileConnectionProvider.idl
+++ b/dom/network/interfaces/nsIMobileConnectionProvider.idl
@@ -1,28 +1,33 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIDOMMozMobileConnectionInfo;
+interface nsIDOMMozMobileNetworkInfo;
 interface nsIDOMDOMRequest;
 interface nsIDOMWindow;
 
 /**
  * XPCOM component (in the content process) that provides the mobile
  * network information.
  */
 [scriptable, uuid(fb3fac34-c1c2-45a9-ad18-a7af0f7997c9)]
 interface nsIMobileConnectionProvider : nsISupports
 {
   readonly attribute DOMString cardState;
   readonly attribute nsIDOMMozMobileConnectionInfo voiceConnectionInfo;
   readonly attribute nsIDOMMozMobileConnectionInfo dataConnectionInfo;
+  readonly attribute DOMString networkSelectionMode;
 
   nsIDOMDOMRequest getNetworks(in nsIDOMWindow window);
+  nsIDOMDOMRequest selectNetwork(in nsIDOMWindow window, in nsIDOMMozMobileNetworkInfo network);
+  nsIDOMDOMRequest selectNetworkAutomatically(in nsIDOMWindow window);
+
   nsIDOMDOMRequest getCardLock(in nsIDOMWindow window, in DOMString lockType);
   nsIDOMDOMRequest unlockCardLock(in nsIDOMWindow window, in jsval info);
   nsIDOMDOMRequest setCardLock(in nsIDOMWindow window, in jsval info);
   nsIDOMDOMRequest sendUSSD(in nsIDOMWindow window, in DOMString ussd);
   nsIDOMDOMRequest cancelUSSD(in nsIDOMWindow window);
 };
--- a/dom/network/src/MobileConnection.cpp
+++ b/dom/network/src/MobileConnection.cpp
@@ -166,28 +166,62 @@ MobileConnection::GetData(nsIDOMMozMobil
   if (!mProvider) {
     *data = nsnull;
     return NS_OK;
   }
   return mProvider->GetDataConnectionInfo(data);
 }
 
 NS_IMETHODIMP
+MobileConnection::GetNetworkSelectionMode(nsAString& networkSelectionMode)
+{
+  if (!mProvider) {
+    networkSelectionMode.SetIsVoid(true);
+    return NS_OK;
+  }
+  return mProvider->GetNetworkSelectionMode(networkSelectionMode);
+}
+
+NS_IMETHODIMP
 MobileConnection::GetNetworks(nsIDOMDOMRequest** request)
 {
   *request = nsnull;
 
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
   return mProvider->GetNetworks(GetOwner(), request);
 }
 
 NS_IMETHODIMP
+MobileConnection::SelectNetwork(nsIDOMMozMobileNetworkInfo* network, nsIDOMDOMRequest** request)
+{
+  *request = nsnull;
+
+  if (!mProvider) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return mProvider->SelectNetwork(GetOwner(), network, request);
+}
+
+NS_IMETHODIMP
+MobileConnection::SelectNetworkAutomatically(nsIDOMDOMRequest** request)
+{
+  *request = nsnull;
+
+  if (!mProvider) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return mProvider->SelectNetworkAutomatically(GetOwner(), request);
+}
+
+NS_IMETHODIMP
 MobileConnection::GetCardLock(const nsAString& aLockType, nsIDOMDOMRequest** aDomRequest)
 {
   *aDomRequest = nsnull;
 
   if (!mProvider) {
     return NS_ERROR_FAILURE;
   }
 
--- a/dom/network/tests/marionette/test_mobile_networks.js
+++ b/dom/network/tests/marionette/test_mobile_networks.js
@@ -1,76 +1,237 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // getNetworks() can take some time..
-MARIONETTE_TIMEOUT = 30000;
+MARIONETTE_TIMEOUT = 60000;
  
 const WHITELIST_PREF = "dom.mobileconnection.whitelist";
 let uriPrePath = window.location.protocol + "//" + window.location.host;
 SpecialPowers.setCharPref(WHITELIST_PREF, uriPrePath);
 
 let connection = navigator.mozMobileConnection;
 ok(connection instanceof MozMobileConnection,
    "connection is instanceof " + connection.constructor);
 
+is(connection.networkSelectionMode, "automatic");
+
+let androidNetwork = null;
+let telkilaNetwork = null;
+
 function isAndroidNetwork(network) {
   is(network.longName, "Android");
   is(network.shortName, "Android");
   is(network.mcc, 310);
   is(network.mnc, 260);
 }
 
+function isTelkilaNetwork(network) {
+  is(network.longName, "TelKila");
+  is(network.shortName, "TelKila");
+  is(network.mcc, 310);
+  is(network.mnc, 295);
+}
+
 function testConnectionInfo() {
   let voice = connection.voice;
   is(voice.connected, true);
   is(voice.emergencyCallsOnly, false);
   is(voice.roaming, false);
   isAndroidNetwork(voice.network);
 
   let data = connection.data;
-  // TODO Bug 762959: enable these checks when data state updates have been implemented
+  // TODO Bug 762959: enable these checks when data state updates are implemented
   // is(data.connected, true);
   // is(data.emergencyCallsOnly, false);
   // is(data.roaming, false);
   isAndroidNetwork(data.network);
 
   testGetNetworks();
 }
 
 function testGetNetworks() {
   let request = connection.getNetworks();
   ok(request instanceof DOMRequest,
-      "request is instanceof " + request.constructor);
+     "request is instanceof " + request.constructor);
 
   request.onerror = function() {
     ok(false, request.error);
-    cleanUp();
+    setTimeout(testSelectNetwork, 0);
   };
 
   request.onsuccess = function() {
     ok('result' in request, "Request did not contain a result");
     let networks = request.result;
 
     // The emulator RIL server should always return 2 networks:
     // {"longName":"Android","shortName":"Android","mcc":310,"mnc":260,"state":"available"}
     // {"longName":"TelKila","shortName":"TelKila","mcc":310,"mnc":295,"state":"available"}
     is(networks.length, 2);
 
-    let network1 = networks[0];
+    let network1 = androidNetwork = networks[0];
     isAndroidNetwork(network1);
     is(network1.state, "available");
 
-    let network2 = networks[1];
-    is(network2.longName, "TelKila");
-    is(network2.shortName, "TelKila");
-    is(network2.mcc, 310);
-    is(network2.mnc, 295);
+    let network2 = telkilaNetwork = networks[1];
+    isTelkilaNetwork(network2);
     is(network2.state, "available");
+
+    setTimeout(testSelectNetwork, 0);
+  };
+}
+
+function testSelectNetwork() {
+  let request = connection.selectNetwork(telkilaNetwork);
+  ok(request instanceof DOMRequest,
+     "request instanceof " + request.constructor);
+
+  connection.addEventListener("voicechange", function voiceChange() {
+    connection.removeEventListener("voicechange", voiceChange);
+
+    isTelkilaNetwork(connection.voice.network);
+    setTimeout(testSelectNetworkAutomatically, 0);
+  });
+
+  request.onsuccess = function() {
+    is(connection.networkSelectionMode, "manual",
+       "selectNetwork sets mode to: " + connection.networkSelectionMode);
+  };
+
+  request.onerror = function() {
+    ok(false, request.error);
+    setTimeout(testSelectNetworkAutomatically, 0);
+  };
+}
+
+function testSelectNetworkAutomatically() {
+  let request = connection.selectNetworkAutomatically();
+  ok(request instanceof DOMRequest,
+     "request instanceof " + request.constructor);
+
+  connection.addEventListener("voicechange", function voiceChange() {
+    connection.removeEventListener("voicechange", voiceChange);
+
+    isAndroidNetwork(connection.voice.network);
+    setTimeout(testSelectNetworkErrors, 0);
+  });
+
+  request.onsuccess = function() {
+    is(connection.networkSelectionMode, "automatic",
+       "selectNetworkAutomatically sets mode to: " +
+       connection.networkSelectionMode);
+  };
+
+  request.onerror = function() {
+    ok(false, request.error);
+    setTimeout(testSelectNetworkErrors, 0);
+  };
+}
+
+function throwsException(fn) {
+  try {
+    fn();
+    ok(false, "function did not throw an exception: " + fn);
+  } catch (e) {
+    ok(true, "function succesfully caught exception: " + e);
+  }
+}
+
+function testSelectNetworkErrors() {
+  throwsException(function() {
+    connection.selectNetwork(null);
+  });
+
+  throwsException(function() {
+    connection.selectNetwork({});
+  });
+
+  connection.addEventListener("voicechange", function voiceChange() {
+    connection.removeEventListener("voicechange", voiceChange);
+    setTimeout(testSelectExistingNetworkManual, 0);
+  });
+
+  let request1 = connection.selectNetwork(telkilaNetwork);
+  request1.onerror = function() {
+    ok(false, request.error);
+    setTimeout(testSelectExistingNetworkManual, 0);
+  };
+
+  // attempt to selectNetwork while one request has already been sent
+  throwsException(function() {
+    connection.selectNetwork(androidNetwork);
+  });
+}
+
+function testSelectExistingNetworkManual() {
+  // When the current network is selected again, the DOMRequest's onsuccess
+  // should be called, but the network shouldn't actually change
+
+  // Telkila should be the currently selected network
+  log("Selecting TelKila (should already be selected");
+  let request = connection.selectNetwork(telkilaNetwork);
+
+  let voiceChanged = false;
+  connection.addEventListener("voicechange", function voiceChange() {
+    connection.removeEventListener("voicechange", voiceChange);
+    voiceChanged = true;
+  });
+
+  function nextTest() {
+    // Switch back to automatic selection to setup the next test
+    let autoRequest = connection.selectNetworkAutomatically();
+    autoRequest.onsuccess = function() {
+      setTimeout(testSelectExistingNetworkAuto, 0);
+    };
+    autoRequest.onerror = function() {
+      ok(false, autoRequest.error);
+      cleanUp();
+    };
+  }
+
+  request.onsuccess = function() {
+    // Give the voicechange event another opportunity to fire
+    setTimeout(function() {
+      is(voiceChanged, false,
+         "voiceNetwork changed while manually selecting Telkila network? " +
+         voiceChanged);
+      nextTest();
+    }, 0);
+  };
+
+  request.onerror = function() {
+    ok(false, request.error);
+    nextTest();
+  };
+}
+
+function testSelectExistingNetworkAuto() {
+  // Now try the same thing but using automatic selection
+  log("Selecting automatically (should already be auto)");
+  let request = connection.selectNetworkAutomatically();
+
+  let voiceChanged = false;
+  connection.addEventListener("voicechange", function voiceChange() {
+    connection.removeEventListener("voicechange", voiceChange);
+    voiceChanged = true;
+  });
+
+  request.onsuccess = function() {
+    // Give the voicechange event another opportunity to fire
+    setTimeout(function() {
+      is(voiceChanged, false,
+         "voiceNetwork changed while automatically selecting network? " +
+         voiceChanged);
+      cleanUp();
+    }, 0);
+  };
+
+  request.onerror = function() {
+    ok(false, request.error);
     cleanUp();
   };
 }
 
 function cleanUp() {
   SpecialPowers.clearUserPref(WHITELIST_PREF);
   finish();
 }
--- a/dom/system/gonk/RILContentHelper.js
+++ b/dom/system/gonk/RILContentHelper.js
@@ -8,31 +8,35 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 
 var RIL = {};
 Cu.import("resource://gre/modules/ril_consts.js", RIL);
 
-const DEBUG = false; // set to true to see debug messages
+// set to true to in ril_consts.js to see debug messages
+const DEBUG = RIL.DEBUG_CONTENT_HELPER;
 
 const RILCONTENTHELPER_CID =
   Components.ID("{472816e1-1fd6-4405-996c-806f9ea68174}");
 const MOBILECONNECTIONINFO_CID =
   Components.ID("{a35cfd39-2d93-4489-ac7d-396475dacb27}");
 const MOBILENETWORKINFO_CID =
   Components.ID("{a6c8416c-09b4-46d1-bf29-6520d677d085}");
 
 const RIL_IPC_MSG_NAMES = [
   "RIL:CardStateChanged",
   "RIL:VoiceInfoChanged",
   "RIL:DataInfoChanged",
   "RIL:EnumerateCalls",
   "RIL:GetAvailableNetworks",
+  "RIL:NetworkSelectionModeChanged",
+  "RIL:SelectNetwork",
+  "RIL:SelectNetworkAuto",
   "RIL:CallStateChanged",
   "RIL:CallError",
   "RIL:GetCardLock:Return:OK",
   "RIL:GetCardLock:Return:KO",
   "RIL:SetCardLock:Return:OK",
   "RIL:SetCardLock:Return:KO",
   "RIL:UnlockCardLock:Return:OK",
   "RIL:UnlockCardLock:Return:KO",
@@ -151,30 +155,110 @@ RILContentHelper.prototype = {
     network.mcc = srcNetwork.mcc;
   },
 
   // nsIRILContentHelper
 
   cardState:           RIL.GECKO_CARDSTATE_UNAVAILABLE,
   voiceConnectionInfo: null,
   dataConnectionInfo:  null,
+  networkSelectionMode: RIL.GECKO_NETWORK_SELECTION_UNKNOWN,
+
+  /**
+   * The network that is currently trying to be selected (or "automatic").
+   * This helps ensure that only one network is selected at a time.
+   */
+  _selectingNetwork: null,
 
   getNetworks: function getNetworks(window) {
     if (window == null) {
       throw Components.Exception("Can't get window object",
                                   Cr.NS_ERROR_UNEXPECTED);
     }
 
     let request = Services.DOMRequest.createRequest(window);
     let requestId = this.getRequestId(request);
 
     cpmm.sendAsyncMessage("RIL:GetAvailableNetworks", requestId);
     return request;
   },
 
+  selectNetwork: function selectNetwork(window, network) {
+    if (window == null) {
+      throw Components.Exception("Can't get window object",
+                                  Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    if (this._selectingNetwork) {
+      throw new Error("Already selecting a network: " + this._selectingNetwork);
+    }
+
+    if (!network) {
+      throw new Error("Invalid network provided: " + network);
+    }
+
+    let mnc = network.mnc;
+    if (!mnc) {
+      throw new Error("Invalid network MNC: " + mnc);
+    }
+
+    let mcc = network.mcc;
+    if (!mcc) {
+      throw new Error("Invalid network MCC: " + mcc);
+    }
+
+    let request = Services.DOMRequest.createRequest(window);
+    let requestId = this.getRequestId(request);
+
+    if (this.networkSelectionMode == RIL.GECKO_NETWORK_SELECTION_MANUAL
+        && this.voiceConnectionInfo.network === network) {
+
+      // Already manually selected this network, so schedule
+      // onsuccess to be fired on the next tick
+      this.dispatchFireRequestSuccess(requestId, null);
+      return request;
+    }
+
+    this._selectingNetwork = network;
+
+    cpmm.sendAsyncMessage("RIL:SelectNetwork", {
+      requestId: requestId,
+      mnc: mnc,
+      mcc: mcc
+    });
+
+    return request;
+  },
+
+  selectNetworkAutomatically: function selectNetworkAutomatically(window) {
+
+    if (window == null) {
+      throw Components.Exception("Can't get window object",
+                                  Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    if (this._selectingNetwork) {
+      throw new Error("Already selecting a network: " + this._selectingNetwork);
+    }
+
+    let request = Services.DOMRequest.createRequest(window);
+    let requestId = this.getRequestId(request);
+
+    if (this.networkSelectionMode == RIL.GECKO_NETWORK_SELECTION_AUTOMATIC) {
+      // Already using automatic selection mode, so schedule
+      // onsuccess to be be fired on the next tick
+      this.dispatchFireRequestSuccess(requestId, null);
+      return request;
+    }
+
+    this._selectingNetwork = "automatic";
+    cpmm.sendAsyncMessage("RIL:SelectNetworkAuto", requestId);
+    return request;
+  },
+
   getCardLock: function getCardLock(window, lockType) {
     if (window == null) {
       throw Components.Exception("Can't get window object",
                                   Cr.NS_ERROR_UNEXPECTED);
     }
     let request = Services.DOMRequest.createRequest(window);
     let requestId = this.getRequestId(request);
     cpmm.sendAsyncMessage("RIL:GetCardLock", {lockType: lockType, requestId: requestId});
@@ -325,38 +409,49 @@ RILContentHelper.prototype = {
   },
 
   // nsIFrameMessageListener
 
   fireRequestSuccess: function fireRequestSuccess(requestId, result) {
     let request = this.takeRequest(requestId);
     if (!request) {
       if (DEBUG) {
-        debug("not firing success for id: " + requestId + ", result: " + JSON.stringify(result));
+        debug("not firing success for id: " + requestId +
+              ", result: " + JSON.stringify(result));
       }
       return;
     }
 
     if (DEBUG) {
-      debug("fire request success, id: " + requestId + ", result: " + JSON.stringify(result));
+      debug("fire request success, id: " + requestId +
+            ", result: " + JSON.stringify(result));
     }
     Services.DOMRequest.fireSuccess(request, result);
   },
 
+  dispatchFireRequestSuccess: function dispatchFireRequestSuccess(requestId, result) {
+    let currentThread = Services.tm.currentThread;
+
+    currentThread.dispatch(this.fireRequestSuccess.bind(this, requestId, result),
+                           Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
   fireRequestError: function fireRequestError(requestId, error) {
     let request = this.takeRequest(requestId);
     if (!request) {
       if (DEBUG) {
-        debug("not firing error for id: " + requestId + ", error: " + JSON.stringify(error));
+        debug("not firing error for id: " + requestId +
+              ", error: " + JSON.stringify(error));
       }
       return;
     }
 
     if (DEBUG) {
-      debug("fire request error, id: " + requestId + ", result: " + JSON.stringify(error));
+      debug("fire request error, id: " + requestId +
+            ", result: " + JSON.stringify(error));
     }
     Services.DOMRequest.fireError(request, error);
   },
 
   receiveMessage: function receiveMessage(msg) {
     let request;
     debug("Received message '" + msg.name + "': " + JSON.stringify(msg.json));
     switch (msg.name) {
@@ -375,16 +470,27 @@ RILContentHelper.prototype = {
         Services.obs.notifyObservers(null, kDataChangedTopic, null);
         break;
       case "RIL:EnumerateCalls":
         this.handleEnumerateCalls(msg.json);
         break;
       case "RIL:GetAvailableNetworks":
         this.handleGetAvailableNetworks(msg.json);
         break;
+      case "RIL:NetworkSelectionModeChanged":
+        this.networkSelectionMode = msg.json.mode;
+        break;
+      case "RIL:SelectNetwork":
+        this.handleSelectNetwork(msg.json,
+                                 RIL.GECKO_NETWORK_SELECTION_MANUAL);
+        break;
+      case "RIL:SelectNetworkAuto":
+        this.handleSelectNetwork(msg.json,
+                                 RIL.GECKO_NETWORK_SELECTION_AUTOMATIC);
+        break;
       case "RIL:CallStateChanged":
         this._deliverTelephonyCallback("callStateChanged",
                                        [msg.json.callIndex, msg.json.state,
                                         msg.json.number, msg.json.isActive]);
         break;
       case "RIL:CallError":
         this._deliverTelephonyCallback("notifyError",
                                         [msg.json.callIndex,
@@ -468,16 +574,27 @@ RILContentHelper.prototype = {
       }
 
       networks[i] = info;
     }
 
     Services.DOMRequest.fireSuccess(request, networks);
   },
 
+  handleSelectNetwork: function handleSelectNetwork(message, mode) {
+    this._selectingNetwork = null;
+    this.networkSelectionMode = mode;
+
+    if (message.error) {
+      this.fireRequestError(message.requestId, message.error);
+    } else {
+      this.fireRequestSuccess(message.requestId, null);
+    }
+  },
+
   _deliverTelephonyCallback: function _deliverTelephonyCallback(name, args) {
     if (!this._telephonyCallbacks) {
       return;
     }
 
     let callbacks = this._telephonyCallbacks.slice();
     for each (let callback in callbacks) {
       if (this._telephonyCallbacks.indexOf(callback) == -1) {
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -7,17 +7,18 @@
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var RIL = {};
 Cu.import("resource://gre/modules/ril_consts.js", RIL);
 
-const DEBUG = false; // set to true to see debug messages
+// set to true in ril_consts.js to see debug messages
+const DEBUG = RIL.DEBUG_RIL;
 
 const RADIOINTERFACELAYER_CID =
   Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}");
 const DATACALLINFO_CID =
   Components.ID("{ef474cd9-94f7-4c05-a31b-29b9de8a10d2}");
 
 const nsIAudioManager = Ci.nsIAudioManager;
 const nsIRadioInterfaceLayer = Ci.nsIRadioInterfaceLayer;
@@ -40,16 +41,18 @@ const RIL_IPC_MSG_NAMES = [
   "RIL:StopTone",
   "RIL:Dial",
   "RIL:HangUp",
   "RIL:AnswerCall",
   "RIL:RejectCall",
   "RIL:HoldCall",
   "RIL:ResumeCall",
   "RIL:GetAvailableNetworks",
+  "RIL:SelectNetwork",
+  "RIL:SelectNetworkAuto",
   "RIL:GetCardLock",
   "RIL:UnlockCardLock",
   "RIL:SetCardLock",
   "RIL:SendUSSD",
   "RIL:CancelUSSD"
 ];
 
 XPCOMUtils.defineLazyServiceGetter(this, "gSmsService",
@@ -244,16 +247,21 @@ RadioInterfaceLayer.prototype = {
         this.holdCall(msg.json);
         break;
       case "RIL:ResumeCall":
         this.resumeCall(msg.json);
         break;
       case "RIL:GetAvailableNetworks":
         this.getAvailableNetworks(msg.json);
         break;
+      case "RIL:SelectNetwork":
+        this.selectNetwork(msg.json);
+        break;
+      case "RIL:SelectNetworkAuto":
+        this.selectNetworkAuto(msg.json);
       case "RIL:GetCardLock":
         this.getCardLock(msg.json);
         break;
       case "RIL:UnlockCardLock":
         this.unlockCardLock(msg.json);
         break;
       case "RIL:SetCardLock":
         this.setCardLock(msg.json);
@@ -297,16 +305,28 @@ RadioInterfaceLayer.prototype = {
         this.handleEnumerateCalls(message.calls);
         break;
       case "callError":
         this.handleCallError(message);
         break;
       case "getAvailableNetworks":
         this.handleGetAvailableNetworks(message);
         break;
+      case "selectNetwork":
+        this.handleSelectNetwork(message);
+        break;
+      case "selectNetworkAuto":
+        this.handleSelectNetworkAuto(message);
+        break;
+      case "networkinfochanged":
+        this.updateNetworkInfo(message);
+        break;
+      case "networkselectionmodechange":
+        this.updateNetworkSelectionMode(message);
+        break;
       case "voiceregistrationstatechange":
         this.updateVoiceConnection(message);
         break;
       case "dataregistrationstatechange":
         this.updateDataConnection(message);
         break;
       case "datacallerror":
         // 3G Network revoked the data connection, possible unavailable APN
@@ -393,42 +413,109 @@ RadioInterfaceLayer.prototype = {
       case "cancelussd":
         this.handleCancelUSSD(message);
         break;
       default:
         throw new Error("Don't know about this message type: " + message.type);
     }
   },
 
+  updateNetworkInfo: function updateNetworkInfo(message) {
+    let voiceMessage = message[RIL.NETWORK_INFO_VOICE_REGISTRATION_STATE];
+    let dataMessage = message[RIL.NETWORK_INFO_DATA_REGISTRATION_STATE];
+    let operatorMessage = message[RIL.NETWORK_INFO_OPERATOR];
+    let selectionMessage = message[RIL.NETWORK_INFO_NETWORK_SELECTION_MODE];
+
+    // Batch the *InfoChanged messages together
+    let voiceInfoChanged = false;
+    if (voiceMessage) {
+      voiceMessage.batch = true;
+      voiceInfoChanged = this.updateVoiceConnection(voiceMessage);
+    }
+
+    let dataInfoChanged = false;
+    if (dataMessage) {
+      dataMessage.batch = true;
+      dataInfoChanged = this.updateDataConnection(dataMessage);
+    }
+
+    let voice = this.rilContext.voice;
+    let data = this.rilContext.data;
+    if (operatorMessage) {
+      if (this.networkChanged(operatorMessage, voice.network)) {
+        voiceInfoChanged = true;
+        voice.network = operatorMessage;
+      }
+
+      if (this.networkChanged(operatorMessage, data.network)) {
+        dataInfoChanged = true;
+        data.network = operatorMessage;
+      }
+    }
+
+    if (voiceInfoChanged) {
+      ppmm.sendAsyncMessage("RIL:VoiceInfoChanged", voice);
+    }
+    if (dataInfoChanged) {
+      ppmm.sendAsyncMessage("RIL:DataInfoChanged", data);
+    }
+
+    if (selectionMessage) {
+      this.updateNetworkSelectionMode(selectionMessage);
+    }
+  },
+
+  /**
+   * Sends the RIL:VoiceInfoChanged message when the voice
+   * connection's state has changed.
+   *
+   * @param state The new voice connection state. When state.batch is true,
+   *              the RIL:VoiceInfoChanged message will not be sent.
+   * @return Whether or not this.radioState.voice was updated
+   */
   updateVoiceConnection: function updateVoiceConnection(state) {
     let voiceInfo = this.rilContext.voice;
+    let regState = state.regState;
     voiceInfo.type = "gsm"; //TODO see bug 726098.
-    if (!state || state.regState == RIL.NETWORK_CREG_STATE_UNKNOWN) {
+    if (!state || regState == RIL.NETWORK_CREG_STATE_UNKNOWN) {
       voiceInfo.connected = false;
       voiceInfo.emergencyCallsOnly = false;
       voiceInfo.roaming = false;
       voiceInfo.network = null;
       voiceInfo.type = null;
       voiceInfo.signalStrength = null;
       voiceInfo.relSignalStrength = null;
       ppmm.sendAsyncMessage("RIL:VoiceThis.RadioState.VoiceChanged",
                             voiceInfo);
       return;
     }
-    voiceInfo.emergencyCallsOnly = state.emergencyCallsOnly;
-    voiceInfo.connected =
-      (state.regState == RIL.NETWORK_CREG_STATE_REGISTERED_HOME) ||
-      (state.regState == RIL.NETWORK_CREG_STATE_REGISTERED_ROAMING);
-    voiceInfo.roaming =
-      voiceInfo.connected &&
-      (state == RIL.NETWORK_CREG_STATE_REGISTERED_ROAMING);    
-    voiceInfo.type =
-      RIL.GECKO_RADIO_TECH[state.radioTech] || null;
-    ppmm.sendAsyncMessage("RIL:VoiceInfoChanged", voiceInfo);
+
+    let isRoaming = regState == RIL.NETWORK_CREG_STATE_REGISTERED_ROAMING;
+    let isHome = regState == RIL.NETWORK_CREG_STATE_REGISTERED_HOME;
+    let isConnected = isRoaming || isHome;
+    let radioTech = RIL.GECKO_RADIO_TECH[state.radioTech] || null;
+
+    // Ensure that we check for changes before sending the message
+    if (voiceInfo.emergencyCallsOnly != state.emergencyCallsOnly ||
+        voiceInfo.connected != isConnected ||
+        voiceInfo.roaming != isRoaming ||
+        voiceInfo.type != radioTech) {
 
+      voiceInfo.emergencyCallsOnly = state.emergencyCallsOnly;
+      voiceInfo.connected = isConnected;
+      voiceInfo.roaming = isRoaming;
+      voiceInfo.type = radioTech;
+
+      // When batch is true, hold off on firing VoiceInfoChanged events
+      if (!state.batch) {
+        ppmm.sendAsyncMessage("RIL:VoiceInfoChanged", voiceInfo);
+      }
+      return true;
+    }
+    return false;
   },
 
   _isDataEnabled: function _isDataEnabled() {
     try {
       return Services.prefs.getBoolPref("ril.data.enabled");
     } catch(ex) {
       return false;
     }
@@ -439,17 +526,17 @@ RadioInterfaceLayer.prototype = {
       return Services.prefs.getBoolPref("ril.data.roaming.enabled");
     } catch(ex) {
       return false;
     }
   },
 
   updateDataConnection: function updateDataConnection(state) {
     if (!this._isDataEnabled()) {
-      return;
+      return false;
     }
 
     let isRegistered =
       state.regState == RIL.NETWORK_CREG_STATE_REGISTERED_HOME ||
         (this._isDataRoamingEnabled() &&
          state.regState == RIL.NETWORK_CREG_STATE_REGISTERED_ROAMING);
     let haveDataConnection =
       state.radioTech != RIL.NETWORK_CREG_TECH_UNKNOWN;
@@ -457,16 +544,17 @@ RadioInterfaceLayer.prototype = {
     if (isRegistered && haveDataConnection) {
       debug("Radio is ready for data connection.");
       // RILNetworkInterface will ignore this if it's already connected.
       RILNetworkInterface.connect();
     }
     //TODO need to keep track of some of the state information, and then
     // notify the content when state changes (connected, technology
     // changes, etc.). This should be done in RILNetworkInterface.
+    return false;
   },
 
   handleSignalStrengthChange: function handleSignalStrengthChange(message) {
     // TODO CDMA, EVDO, LTE, etc. (see bug 726098)
     this.rilContext.voice.signalStrength = message.gsmDBM;
     this.rilContext.voice.relSignalStrength = message.gsmRelative;
     ppmm.sendAsyncMessage("RIL:VoiceInfoChanged", this.rilContext.voice);
 
@@ -609,16 +697,40 @@ RadioInterfaceLayer.prototype = {
    */
   handleGetAvailableNetworks: function handleGetAvailableNetworks(message) {
     debug("handleGetAvailableNetworks: " + JSON.stringify(message));
 
     ppmm.sendAsyncMessage("RIL:GetAvailableNetworks", message);
   },
 
   /**
+   * Update network selection mode
+   */
+  updateNetworkSelectionMode: function updateNetworkSelectionMode(message) {
+    debug("updateNetworkSelectionMode: " + JSON.stringify(message));
+    ppmm.sendAsyncMessage("RIL:NetworkSelectionModeChanged", message);
+  },
+
+  /**
+   * Handle "manual" network selection request.
+   */
+  handleSelectNetwork: function handleSelectNetwork(message) {
+    debug("handleSelectNetwork: " + JSON.stringify(message));
+    ppmm.sendAsyncMessage("RIL:SelectNetwork", message);
+  },
+
+  /**
+   * Handle "automatic" network selection request.
+   */
+  handleSelectNetworkAuto: function handleSelectNetworkAuto(message) {
+    debug("handleSelectNetworkAuto: " + JSON.stringify(message));
+    ppmm.sendAsyncMessage("RIL:SelectNetworkAuto", message);
+  },
+
+  /**
    * Handle call error.
    */
   handleCallError: function handleCallError(message) {
     ppmm.sendAsyncMessage("RIL:CallError", message);   
   },
 
   /**
    * Handle WDP port push PDU. Constructor WDP bearer information and deliver
@@ -946,16 +1058,26 @@ RadioInterfaceLayer.prototype = {
   },
 
   cancelUSSD: function cancelUSSD(message) {
     debug("Cancel pending USSD");
     message.type = "cancelUSSD";
     this.worker.postMessage(message);
   },
 
+  selectNetworkAuto: function selectNetworkAuto(requestId) {
+    this.worker.postMessage({type: "selectNetworkAuto", requestId: requestId});
+  },
+
+  selectNetwork: function selectNetwork(message) {
+    message.type = "selectNetwork";
+    this.worker.postMessage(message);
+  },
+
+
   get microphoneMuted() {
     return gAudioManager.microphoneMuted;
   },
   set microphoneMuted(value) {
     if (value == this.microphoneMuted) {
       return;
     }
     gAudioManager.phoneState = value ?
--- a/dom/system/gonk/ril_consts.js
+++ b/dom/system/gonk/ril_consts.js
@@ -1,12 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// Set to true to debug all RIL layers
+const DEBUG_ALL = false;
+
+// Set individually to debug specific layers
+const DEBUG_WORKER = false || DEBUG_ALL;
+const DEBUG_CONTENT_HELPER = false || DEBUG_ALL;
+const DEBUG_RIL = false || DEBUG_ALL;
+
 const REQUEST_GET_SIM_STATUS = 1;
 const REQUEST_ENTER_SIM_PIN = 2;
 const REQUEST_ENTER_SIM_PUK = 3;
 const REQUEST_ENTER_SIM_PIN2 = 4;
 const REQUEST_ENTER_SIM_PUK2 = 5;
 const REQUEST_CHANGE_SIM_PIN = 6;
 const REQUEST_CHANGE_SIM_PIN2 = 7;
 const REQUEST_ENTER_NETWORK_DEPERSONALIZATION = 8;
@@ -206,22 +214,24 @@ const ERROR_SS_MODIFIED_TO_DIAL = 23;
 const ERROR_SS_MODIFIED_TO_USSD = 24;
 const ERROR_SS_MODIFIED_TO_SS = 25;
 const ERROR_SUBSCRIPTION_NOT_SUPPORTED = 26;
 
 const GECKO_ERROR_SUCCESS = null;
 const GECKO_ERROR_RADIO_NOT_AVAILABLE = "RadioNotAvailable";
 const GECKO_ERROR_GENERIC_FAILURE = "GenericFailure";
 const GECKO_ERROR_REQUEST_NOT_SUPPORTED = "RequestNotSupported";
+const GECKO_ERROR_ILLEGAL_SIM_OR_ME = "IllegalSIMorME";
 
 const RIL_ERROR_TO_GECKO_ERROR = {};
 RIL_ERROR_TO_GECKO_ERROR[ERROR_SUCCESS] = GECKO_ERROR_SUCCESS;
 RIL_ERROR_TO_GECKO_ERROR[ERROR_RADIO_NOT_AVAILABLE] = GECKO_ERROR_RADIO_NOT_AVAILABLE;
 RIL_ERROR_TO_GECKO_ERROR[ERROR_GENERIC_FAILURE] = GECKO_ERROR_GENERIC_FAILURE;
 RIL_ERROR_TO_GECKO_ERROR[ERROR_REQUEST_NOT_SUPPORTED] = GECKO_ERROR_REQUEST_NOT_SUPPORTED;
+RIL_ERROR_TO_GECKO_ERROR[ERROR_ILLEGAL_SIM_OR_ME] = GECKO_ERROR_ILLEGAL_SIM_OR_ME;
 
 // 3GPP 23.040 clause 9.2.3.6 TP-Message-Reference(TP-MR):
 // The number of times the MS automatically repeats the SMS-SUBMIT shall be in
 // the range 1 to 3 but the precise number is an implementation matter.
 const SMS_RETRY_MAX = 3;
 
 const RADIO_STATE_OFF = 0;
 const RADIO_STATE_UNAVAILABLE = 1;
@@ -296,16 +306,27 @@ const CARD_MAX_APPS = 8;
 const NETWORK_STATE_UNKNOWN = "unknown";
 const NETWORK_STATE_AVAILABLE = "available";
 const NETWORK_STATE_CONNECTED = "connected";
 const NETWORK_STATE_FORBIDDEN = "forbidden";
 
 const NETWORK_SELECTION_MODE_AUTOMATIC = 0;
 const NETWORK_SELECTION_MODE_MANUAL = 1;
 
+const NETWORK_INFO_VOICE_REGISTRATION_STATE = "voiceRegistrationState";
+const NETWORK_INFO_DATA_REGISTRATION_STATE = "dataRegistrationState";
+const NETWORK_INFO_OPERATOR = "operator";
+const NETWORK_INFO_NETWORK_SELECTION_MODE = "networkSelectionMode";
+const NETWORK_INFO_MESSAGE_TYPES = [
+  NETWORK_INFO_VOICE_REGISTRATION_STATE,
+  NETWORK_INFO_DATA_REGISTRATION_STATE,
+  NETWORK_INFO_OPERATOR,
+  NETWORK_INFO_NETWORK_SELECTION_MODE
+];
+
 const PREFERRED_NETWORK_TYPE_GSM_WCDMA = 0;
 const PREFERRED_NETWORK_TYPE_GSM_ONLY = 1;
 const PREFERRED_NETWORK_TYPE_WCDMA = 2;
 const PREFERRED_NETWORK_TYPE_GSM_WCDMA_AUTO = 3;
 const PREFERRED_NETWORK_TYPE_CDMA_EVDO_AUTO = 4;
 const PREFERRED_NETWORK_TYPE_CDMA_ONLY = 5;
 const PREFERRED_NETWORK_TYPE_EVDO_ONLY = 6;
 const PREFERRED_NETWORK_TYPE_GSM_WCDMA_CDMA_EVDO_AUTO = 7;
@@ -1389,16 +1410,20 @@ const GECKO_RADIOSTATE_READY         = "
 const GECKO_CARDSTATE_UNAVAILABLE    = null;
 const GECKO_CARDSTATE_ABSENT         = "absent";
 const GECKO_CARDSTATE_PIN_REQUIRED   = "pin_required";
 const GECKO_CARDSTATE_PUK_REQUIRED   = "puk_required";
 const GECKO_CARDSTATE_NETWORK_LOCKED = "network_locked";
 const GECKO_CARDSTATE_NOT_READY      = null;
 const GECKO_CARDSTATE_READY          = "ready";
 
+const GECKO_NETWORK_SELECTION_UNKNOWN   = null;
+const GECKO_NETWORK_SELECTION_AUTOMATIC = "automatic";
+const GECKO_NETWORK_SELECTION_MANUAL    = "manual";
+
 const GECKO_CALL_ERROR_BAD_NUMBER             = "BadNumberError";
 const GECKO_CALL_ERROR_NORMAL_CALL_CLEARING   = "NormalCallClearingError";
 const GECKO_CALL_ERROR_BUSY                   = "BusyError";
 const GECKO_CALL_ERROR_CONGESTION             = "CongestionError";
 const GECKO_CALL_ERROR_INCOMING_CALL_EXCEEDED = "IncomingCallExceededError";
 const GECKO_CALL_ERROR_BARRED                 = "BarredError";
 const GECKO_CALL_ERROR_FDN_BLOCKED            = "FDNBlockedError";
 const GECKO_CALL_ERROR_SUBSCRIBER_UNKNOWN     = "SubscriberUnknownError";
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -24,20 +24,18 @@
  * terms of object allocations. As a result, it may look more like C than
  * JavaScript, and that's intended.
  */
 
 "use strict";
 
 importScripts("ril_consts.js", "systemlibs.js");
 
-// We leave this as 'undefined' instead of setting it to 'false'. That
-// way an outer scope can define it to 'true' (e.g. for testing purposes)
-// without us overriding that here.
-let DEBUG;
+// set to true in ril_consts.js to see debug messages
+let DEBUG = DEBUG_WORKER;
 
 const INT32_MAX   = 2147483647;
 const UINT8_SIZE  = 1;
 const UINT16_SIZE = 2;
 const UINT32_SIZE = 4;
 const PARCEL_SIZE_SIZE = UINT32_SIZE;
 
 const PDU_HEX_OCTET_SIZE = 4;
@@ -515,16 +513,17 @@ let Buf = {
    * @param type
    *        Integer specifying the request type.
    * @param options [optional]
    *        Object containing information about the request, e.g. the
    *        original main thread message object that led to the RIL request.
    */
   newParcel: function newParcel(type, options) {
     if (DEBUG) debug("New outgoing parcel of type " + type);
+
     // We're going to leave room for the parcel size at the beginning.
     this.outgoingIndex = PARCEL_SIZE_SIZE;
     this.writeUint32(type);
     let token = this.token;
     this.writeUint32(token);
 
     if (!options) {
       options = {};
@@ -612,21 +611,16 @@ let RIL = {
   operator: null,
 
   /**
    * String containing the baseband version.
    */
   basebandVersion: null,
 
   /**
-   * Network selection mode. 0 for automatic, 1 for manual selection.
-   */
-  networkSelectionMode: null,
-
-  /**
    * Valid calls.
    */
   currentCalls: {},
 
   /**
    * Current calls length.
    */
   currentCallsLength: null,
@@ -644,16 +638,27 @@ let RIL = {
   _receivedSmsSegmentsMap: {},
 
   /**
    * Outgoing messages waiting for SMS-STATUS-REPORT.
    */
   _pendingSentSmsMap: {},
 
   /**
+   * Whether or not the multiple requests in requestNetworkInfo() are currently
+   * being processed
+   */
+  _processingNetworkInfo: false,
+
+  /**
+   * Pending messages to be send in batch from requestNetworkInfo()
+   */
+  _pendingNetworkInfo: {type: "networkinfochanged"},
+
+  /**
    * Mute or unmute the radio.
    */
   _muted: true,
   get muted() {
     return this._muted;
   },
   set muted(val) {
     val = Boolean(val);
@@ -1357,57 +1362,89 @@ let RIL = {
   getDataRegistrationState: function getDataRegistrationState() {
     Buf.simpleRequest(REQUEST_DATA_REGISTRATION_STATE);
   },
 
   getOperator: function getOperator() {
     Buf.simpleRequest(REQUEST_OPERATOR);
   },
 
-  getNetworkSelectionMode: function getNetworkSelectionMode() {
-    Buf.simpleRequest(REQUEST_QUERY_NETWORK_SELECTION_MODE);
-  },
-
-  setNetworkSelectionAutomatic: function setNetworkSelectionAutomatic() {
-    Buf.simpleRequest(REQUEST_SET_NETWORK_SELECTION_AUTOMATIC);
-  },
-
   /**
    * Set the preferred network type.
    *
    * @param network_type
    *        The network type. One of the PREFERRED_NETWORK_TYPE_* constants.
    */
   setPreferredNetworkType: function setPreferredNetworkType(network_type) {
     Buf.newParcel(REQUEST_SET_PREFERRED_NETWORK_TYPE);
     Buf.writeUint32(network_type);
     Buf.sendParcel();
   },
 
   /**
    * Request various states about the network.
    */
   requestNetworkInfo: function requestNetworkInfo() {
-    if (DEBUG) debug("Requesting phone state");
+    if (this._processingNetworkInfo) {
+      if (DEBUG) {
+        debug("Already requesting network info: " +
+              JSON.stringify(this._pendingNetworkInfo))
+      }
+      return;
+    }
+
+    if (DEBUG) debug("Requesting network info");
+
+    this._processingNetworkInfo = true;
     this.getVoiceRegistrationState();
     this.getDataRegistrationState(); //TODO only GSM
     this.getOperator();
     this.getNetworkSelectionMode();
   },
 
   /**
    * Get the available networks
    */
   getAvailableNetworks: function getAvailableNetworks(options) {
     if (DEBUG) debug("Getting available networks");
     Buf.newParcel(REQUEST_QUERY_AVAILABLE_NETWORKS, options);
     Buf.sendParcel();
   },
 
   /**
+   * Request the radio's network selection mode
+   */
+  getNetworkSelectionMode: function getNetworkSelectionMode(options) {
+    if (DEBUG) debug("Getting network selection mode");
+    Buf.simpleRequest(REQUEST_QUERY_NETWORK_SELECTION_MODE, options);
+  },
+
+  /**
+   * Tell the radio to automatically choose a voice/data network
+   */
+  selectNetworkAuto: function selectNetworkAuto(options) {
+    if (DEBUG) debug("Setting automatic network selection");
+    Buf.simpleRequest(REQUEST_SET_NETWORK_SELECTION_AUTOMATIC, options);
+  },
+
+  /**
+   * Tell the radio to choose a specific voice/data network
+   */
+  selectNetwork: function selectNetwork(options) {
+    if (DEBUG) {
+      debug("Setting manual network selection: " + options.mcc + options.mnc);
+    }
+
+    let numeric = String(options.mcc) + options.mnc;
+    Buf.newParcel(REQUEST_SET_NETWORK_SELECTION_MANUAL, options);
+    Buf.writeString(numeric);
+    Buf.sendParcel();
+  },
+
+  /**
    * Get current calls.
    */
   getCurrentCalls: function getCurrentCalls() {
     Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS);
   },
 
   /**
    * Get the signal strength.
@@ -1843,30 +1880,33 @@ let RIL = {
         break;
       case CARD_APPSTATE_PUK:
         newCardState = GECKO_CARDSTATE_PUK_REQUIRED;
         break;
       case CARD_APPSTATE_SUBSCRIPTION_PERSO:
         newCardState = GECKO_CARDSTATE_NETWORK_LOCKED;
         break;
       case CARD_APPSTATE_READY:
-        this.requestNetworkInfo();
-        this.getSignalStrength();
-        this.fetchICCRecords();
         newCardState = GECKO_CARDSTATE_READY;
         break;
       case CARD_APPSTATE_UNKNOWN:
       case CARD_APPSTATE_DETECTED:
       default:
         newCardState = GECKO_CARDSTATE_NOT_READY;
     }
 
     if (this.cardState == newCardState) {
       return;
     }
+
+    // This was moved down from CARD_APPSTATE_READY
+    this.requestNetworkInfo();
+    this.getSignalStrength();
+    this.fetchICCRecords();
+
     this.cardState = newCardState;
     this.sendDOMMessage({type: "cardstatechange",
                          cardState: this.cardState});
   },
 
   /**
    * Process a ICC_COMMAND_GET_RESPONSE type command for REQUEST_SIM_IO.
    */
@@ -1973,16 +2013,92 @@ let RIL = {
         break;
 
       case ICC_COMMAND_READ_BINARY:
         this._processICCIOReadBinary(options);
         break;
     } 
   },
 
+  // We combine all of the NETWORK_INFO_MESSAGE_TYPES into one "networkinfochange"
+  // message to the RadioInterfaceLayer, so we can avoid sending multiple
+  // VoiceInfoChanged events for both operator / voice_data_registration
+  //
+  // State management here is a little tricky. We need to know both:
+  // 1. Whether or not a response was received for each of the
+  //    NETWORK_INFO_MESSAGE_TYPES
+  // 2. The outbound message that corresponds with that response -- but this
+  //    only happens when internal state changes (i.e. it isn't guaranteed)
+  //
+  // To collect this state, each message response function first calls
+  // _receivedNetworkInfo, to mark the response as received. When the
+  // final response is received, a call to _sendPendingNetworkInfo is placed
+  // on the next tick of the worker thread.
+  //
+  // Since the original call to _receivedNetworkInfo happens at the top
+  // of the response handler, this gives the final handler a chance to
+  // queue up it's "changed" message by calling _sendNetworkInfoMessage if/when
+  // the internal state has actually changed.
+  _sendNetworkInfoMessage: function _sendNetworkInfoMessage(type, message) {
+    if (!this._processingNetworkInfo) {
+      // We only combine these messages in the case of the combined request
+      // in requestNetworkInfo()
+      this.sendDOMMessage(message);
+      return;
+    }
+
+    this._pendingNetworkInfo[type] = message;
+  },
+
+  _receivedNetworkInfo: function _receivedNetworkInfo(type) {
+    if (!this._processingNetworkInfo) {
+      return;
+    }
+
+    let pending = this._pendingNetworkInfo;
+
+    // We still need to track states for events that aren't fired
+    if (!(type in pending)) {
+      pending[type] = true;
+    }
+
+    // Pending network info is ready to be sent when no more messages
+    // are waiting for responses, but the combined payload hasn't been sent.
+    for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) {
+      let type = NETWORK_INFO_MESSAGE_TYPES[i];
+      if (!(type in pending)) {
+        return;
+      }
+    }
+
+    // Do a pass to clean up the processed messages that didn't create
+    // a response message, so we don't have unused keys in the outbound
+    // networkinfochanged message
+    let keys = Object.keys(pending);
+    for (let i = 0; i < keys.length; i++) {
+      let key = keys[i];
+      if (pending[key] === true) {
+        delete pending[key];
+      }
+    }
+
+    // Send the message on the next tick of the worker's loop, so we give the
+    // last message a chance to call _sendNetworkInfoMessage first.
+    setTimeout(this._sendPendingNetworkInfo.bind(this), 0);
+  },
+
+  _sendPendingNetworkInfo: function _sendPendingNetworkInfo() {
+    this.sendDOMMessage(this._pendingNetworkInfo);
+
+    this._processingNetworkInfo = false;
+    for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) {
+      delete this._pendingNetworkInfo[NETWORK_INFO_MESSAGE_TYPES[i]];
+    }
+  },
+
   _processVoiceRegistrationState: function _processVoiceRegistrationState(state) {
     this.initRILQuirks();
 
     let rs = this.voiceRegistrationState;
     let stateChanged = false;
 
     let regState = RIL.parseInt(state[0], NETWORK_CREG_STATE_UNKNOWN);
     if (rs.regState != regState) {
@@ -2047,17 +2163,17 @@ let RIL = {
       let roamingIndicator = RIL.parseInt(state[10]);
       let systemIsInPRL = RIL.parseInt(state[11]);
       let defaultRoamingIndicator = RIL.parseInt(state[12]);
       let reasonForDenial = RIL.parseInt(state[13]);
     }
 
     if (stateChanged) {
       rs.type = "voiceregistrationstatechange";
-      this.sendDOMMessage(rs);
+      this._sendNetworkInfoMessage(NETWORK_INFO_VOICE_REGISTRATION_STATE, rs);
     }
   },
 
   _processDataRegistrationState: function _processDataRegistrationState(state) {
     let rs = this.dataRegistrationState;
     let stateChanged = false;
 
     let regState = RIL.parseInt(state[0], NETWORK_CREG_STATE_UNKNOWN);
@@ -2069,17 +2185,17 @@ let RIL = {
     let radioTech = RIL.parseInt(state[3], NETWORK_CREG_TECH_UNKNOWN);
     if (rs.radioTech != radioTech) {
       rs.radioTech = radioTech;
       stateChanged = true;
     }
 
     if (stateChanged) {
       rs.type = "dataregistrationstatechange";
-      this.sendDOMMessage(rs);
+      this._sendNetworkInfoMessage(NETWORK_INFO_DATA_REGISTRATION_STATE, rs);
     }
   },
 
   /**
    * Helpers for processing call state and handle the active call.
    */
   _processCalls: function _processCalls(newCalls) {
     // Go through the calls we currently have on file and see if any of them
@@ -2822,38 +2938,46 @@ RIL[REQUEST_SIGNAL_STRENGTH] = function 
     obj.lteCQI = Buf.readUint32();
   }
 
   if (DEBUG) debug("Signal strength " + JSON.stringify(obj));
   obj.type = "signalstrengthchange";
   this.sendDOMMessage(obj);
 };
 RIL[REQUEST_VOICE_REGISTRATION_STATE] = function REQUEST_VOICE_REGISTRATION_STATE(length, options) {
+  this._receivedNetworkInfo(NETWORK_INFO_VOICE_REGISTRATION_STATE);
+
   if (options.rilRequestError) {
     return;
   }
 
   let state = Buf.readStringList();
   if (DEBUG) debug("voice registration state: " + state);
+
   this._processVoiceRegistrationState(state);
 };
 RIL[REQUEST_DATA_REGISTRATION_STATE] = function REQUEST_DATA_REGISTRATION_STATE(length, options) {
+  this._receivedNetworkInfo(NETWORK_INFO_DATA_REGISTRATION_STATE);
+
   if (options.rilRequestError) {
     return;
   }
 
   let state = Buf.readStringList();
   this._processDataRegistrationState(state);
 };
 RIL[REQUEST_OPERATOR] = function REQUEST_OPERATOR(length, options) {
+  this._receivedNetworkInfo(NETWORK_INFO_OPERATOR);
+
   if (options.rilRequestError) {
     return;
   }
 
   let operator = Buf.readStringList();
+
   if (DEBUG) debug("Operator data: " + operator);
   if (operator.length < 3) {
     if (DEBUG) debug("Expected at least 3 strings for operator.");
   }
 
   if (!this.operator) {
     this.operator = {type: "operatorchange"};
   }
@@ -2870,17 +2994,17 @@ RIL[REQUEST_OPERATOR] = function REQUEST
 
     let networkTuple = operator[2];
     try {
       this._processNetworkTuple(networkTuple, this.operator);
     } catch (e) {
       debug("Error processing operator tuple: " + e);
     }
 
-   this.sendDOMMessage(this.operator);
+    this._sendNetworkInfoMessage(NETWORK_INFO_OPERATOR, this.operator);
   }
 };
 RIL[REQUEST_RADIO_POWER] = null;
 RIL[REQUEST_DTMF] = null;
 RIL[REQUEST_SEND_SMS] = function REQUEST_SEND_SMS(length, options) {
   if (options.rilRequestError) {
     switch (options.rilRequestError) {
       case ERROR_SMS_SEND_FAIL_RETRY:
@@ -3045,25 +3169,63 @@ RIL[REQUEST_SET_FACILITY_LOCK] = functio
   this.sendDOMMessage({type: "iccsetcardlock",
                        lockType: "pin",
                        result: options.rilRequestError == 0 ? true : false,
                        retryCount: length ? Buf.readUint32List()[0] : -1,
                        requestId: options.requestId});
 };
 RIL[REQUEST_CHANGE_BARRING_PASSWORD] = null;
 RIL[REQUEST_QUERY_NETWORK_SELECTION_MODE] = function REQUEST_QUERY_NETWORK_SELECTION_MODE(length, options) {
+  this._receivedNetworkInfo(NETWORK_INFO_NETWORK_SELECTION_MODE);
+
   if (options.rilRequestError) {
+    options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendDOMMessage(options);
     return;
   }
 
   let mode = Buf.readUint32List();
-  this.networkSelectionMode = mode[0];
+  let selectionMode;
+
+  switch (mode[0]) {
+    case NETWORK_SELECTION_MODE_AUTOMATIC:
+      selectionMode = GECKO_NETWORK_SELECTION_AUTOMATIC;
+      break;
+    case NETWORK_SELECTION_MODE_MANUAL:
+      selectionMode = GECKO_NETWORK_SELECTION_MANUAL;
+      break;
+    default:
+      selectionMode = GECKO_NETWORK_SELECTION_UNKNOWN;
+      break;
+  }
+
+  if (this.mode != selectionMode) {
+    this.mode = options.mode = selectionMode;
+    options.type = "networkselectionmodechange";
+    this._sendNetworkInfoMessage(NETWORK_INFO_NETWORK_SELECTION_MODE, options);
+  }
 };
-RIL[REQUEST_SET_NETWORK_SELECTION_AUTOMATIC] = null;
-RIL[REQUEST_SET_NETWORK_SELECTION_MANUAL] = null;
+RIL[REQUEST_SET_NETWORK_SELECTION_AUTOMATIC] = function REQUEST_SET_NETWORK_SELECTION_AUTOMATIC(length, options) {
+  if (options.rilRequestError) {
+    options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendDOMMessage(options);
+    return;
+  }
+
+  this.sendDOMMessage(options);
+};
+RIL[REQUEST_SET_NETWORK_SELECTION_MANUAL] = function REQUEST_SET_NETWORK_SELECTION_MANUAL(length, options) {
+  if (options.rilRequestError) {
+    options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendDOMMessage(options);
+    return;
+  }
+
+  this.sendDOMMessage(options);
+};
 RIL[REQUEST_QUERY_AVAILABLE_NETWORKS] = function REQUEST_QUERY_AVAILABLE_NETWORKS(length, options) {
   if (options.rilRequestError) {
     options.error = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
     this.sendDOMMessage(options);
     return;
   }
 
   options.networks = this._processNetworks();