/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* 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/. */

"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/systemlibs.js");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/WifiCommand.jsm");
Cu.import("resource://gre/modules/WifiNetUtil.jsm");
Cu.import("resource://gre/modules/WifiP2pManager.jsm");
Cu.import("resource://gre/modules/WifiP2pWorkerObserver.jsm");

var DEBUG = false; // set to true to show debug messages.

const WIFIWORKER_CONTRACTID = "@mozilla.org/wifi/worker;1";
const WIFIWORKER_CID        = Components.ID("{a14e8977-d259-433a-a88d-58dd44657e5b}");

const WIFIWORKER_WORKER     = "resource://gre/modules/wifi_worker.js";

const kMozSettingsChangedObserverTopic   = "mozsettings-changed";

const MAX_RETRIES_ON_AUTHENTICATION_FAILURE = 2;
const MAX_SUPPLICANT_LOOP_ITERATIONS = 4;
const MAX_RETRIES_ON_DHCP_FAILURE = 2;

// Settings DB path for wifi
const SETTINGS_WIFI_ENABLED            = "wifi.enabled";
const SETTINGS_WIFI_DEBUG_ENABLED      = "wifi.debugging.enabled";
// Settings DB path for Wifi tethering.
const SETTINGS_WIFI_TETHERING_ENABLED  = "tethering.wifi.enabled";
const SETTINGS_WIFI_SSID               = "tethering.wifi.ssid";
const SETTINGS_WIFI_SECURITY_TYPE      = "tethering.wifi.security.type";
const SETTINGS_WIFI_SECURITY_PASSWORD  = "tethering.wifi.security.password";
const SETTINGS_WIFI_IP                 = "tethering.wifi.ip";
const SETTINGS_WIFI_PREFIX             = "tethering.wifi.prefix";
const SETTINGS_WIFI_DHCPSERVER_STARTIP = "tethering.wifi.dhcpserver.startip";
const SETTINGS_WIFI_DHCPSERVER_ENDIP   = "tethering.wifi.dhcpserver.endip";
const SETTINGS_WIFI_DNS1               = "tethering.wifi.dns1";
const SETTINGS_WIFI_DNS2               = "tethering.wifi.dns2";

// Settings DB path for USB tethering.
const SETTINGS_USB_DHCPSERVER_STARTIP  = "tethering.usb.dhcpserver.startip";
const SETTINGS_USB_DHCPSERVER_ENDIP    = "tethering.usb.dhcpserver.endip";

// Default value for WIFI tethering.
const DEFAULT_WIFI_IP                  = "192.168.1.1";
const DEFAULT_WIFI_PREFIX              = "24";
const DEFAULT_WIFI_DHCPSERVER_STARTIP  = "192.168.1.10";
const DEFAULT_WIFI_DHCPSERVER_ENDIP    = "192.168.1.30";
const DEFAULT_WIFI_SSID                = "FirefoxHotspot";
const DEFAULT_WIFI_SECURITY_TYPE       = "open";
const DEFAULT_WIFI_SECURITY_PASSWORD   = "1234567890";
const DEFAULT_DNS1                     = "8.8.8.8";
const DEFAULT_DNS2                     = "8.8.4.4";

// Default value for USB tethering.
const DEFAULT_USB_DHCPSERVER_STARTIP   = "192.168.0.10";
const DEFAULT_USB_DHCPSERVER_ENDIP     = "192.168.0.30";

const WIFI_FIRMWARE_AP            = "AP";
const WIFI_FIRMWARE_STATION       = "STA";
const WIFI_SECURITY_TYPE_NONE     = "open";
const WIFI_SECURITY_TYPE_WPA_PSK  = "wpa-psk";
const WIFI_SECURITY_TYPE_WPA2_PSK = "wpa2-psk";

const NETWORK_INTERFACE_UP   = "up";
const NETWORK_INTERFACE_DOWN = "down";

const DEFAULT_WLAN_INTERFACE = "wlan0";

const DRIVER_READY_WAIT = 2000;

const SUPP_PROP = "init.svc.wpa_supplicant";
const WPA_SUPPLICANT = "wpa_supplicant";
const DHCP_PROP = "init.svc.dhcpcd";
const DHCP = "dhcpcd";

const MODE_ESS = 0;
const MODE_IBSS = 1;

XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager",
                                   "@mozilla.org/network/manager;1",
                                   "nsINetworkManager");

XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService",
                                   "@mozilla.org/network/service;1",
                                   "nsINetworkService");

XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
                                   "@mozilla.org/settingsService;1",
                                   "nsISettingsService");

XPCOMUtils.defineLazyServiceGetter(this, "gTetheringService",
                                   "@mozilla.org/tethering/service;1",
                                   "nsITetheringService");

// A note about errors and error handling in this file:
// The libraries that we use in this file are intended for C code. For
// C code, it is natural to return -1 for errors and 0 for success.
// Therefore, the code that interacts directly with the worker uses this
// convention (note: command functions do get boolean results since the
// command always succeeds and we do a string/boolean check for the
// expected results).
var WifiManager = (function() {
  var manager = {};

  function getStartupPrefs() {
    return {
      sdkVersion: parseInt(libcutils.property_get("ro.build.version.sdk"), 10),
      unloadDriverEnabled: libcutils.property_get("ro.moz.wifi.unloaddriver") === "1",
      schedScanRecovery: libcutils.property_get("ro.moz.wifi.sched_scan_recover") === "false" ? false : true,
      driverDelay: libcutils.property_get("ro.moz.wifi.driverDelay"),
      p2pSupported: libcutils.property_get("ro.moz.wifi.p2p_supported") === "1",
      eapSimSupported: libcutils.property_get("ro.moz.wifi.eapsim_supported") === "1",
      ibssSupported: libcutils.property_get("ro.moz.wifi.ibss_supported", "true") === "true",
      ifname: libcutils.property_get("wifi.interface")
    };
  }

  let {sdkVersion, unloadDriverEnabled, schedScanRecovery,
       driverDelay, p2pSupported, eapSimSupported, ibssSupported, ifname} = getStartupPrefs();

  let capabilities = {
    security: ["OPEN", "WEP", "WPA-PSK", "WPA-EAP"],
    eapMethod: ["PEAP", "TTLS", "TLS"],
    eapPhase2: ["MSCHAPV2"],
    certificate: ["SERVER"],
    mode: [MODE_ESS]
  };
  if (eapSimSupported) {
    capabilities.eapMethod.unshift("SIM");
  }
  if (ibssSupported) {
    capabilities.mode.push(MODE_IBSS);
  }

  let wifiListener = {
    onWaitEvent: function(event, iface) {
      if (manager.ifname === iface && handleEvent(event)) {
        waitForEvent(iface);
      } else if (p2pSupported) {
        // Please refer to
        // http://androidxref.com/4.4.2_r1/xref/frameworks/base/wifi/java/android/net/wifi/WifiMonitor.java#519
        // for interface event mux/demux rules. In short words, both
        // 'p2p0' and 'p2p-' should go to Wifi P2P state machine.
        if (WifiP2pManager.INTERFACE_NAME === iface || -1 !== iface.indexOf('p2p-')) {
          // If the connection is closed, wifi.c::wifi_wait_for_event()
          // will still return 'CTRL-EVENT-TERMINATING  - connection closed'
          // rather than blocking. So when we see this special event string,
          // just return immediately.
          const TERMINATED_EVENT = 'CTRL-EVENT-TERMINATING  - connection closed';
          if (-1 !== event.indexOf(TERMINATED_EVENT)) {
            return;
          }
          p2pManager.handleEvent(event);
          waitForEvent(iface);
        }
      }
    },

    onCommand: function(event, iface) {
      onmessageresult(event, iface);
    }
  }

  manager.ifname = ifname;
  manager.connectToSupplicant = false;
  // Emulator build runs to here.
  // The debug() should only be used after WifiManager.
  if (!ifname) {
    manager.ifname = DEFAULT_WLAN_INTERFACE;
  }
  manager.schedScanRecovery = schedScanRecovery;
  manager.driverDelay = driverDelay ? parseInt(driverDelay, 10) : DRIVER_READY_WAIT;

  // Regular Wifi stuff.
  var netUtil = WifiNetUtil(controlMessage);
  var wifiCommand = WifiCommand(controlMessage, manager.ifname, sdkVersion);

  // Wifi P2P stuff
  var p2pManager;
  if (p2pSupported) {
    let p2pCommand = WifiCommand(controlMessage, WifiP2pManager.INTERFACE_NAME, sdkVersion);
    p2pManager = WifiP2pManager(p2pCommand, netUtil);
  }

  let wifiService = Cc["@mozilla.org/wifi/service;1"];
  if (wifiService) {
    wifiService = wifiService.getService(Ci.nsIWifiProxyService);
    let interfaces = [manager.ifname];
    if (p2pSupported) {
      interfaces.push(WifiP2pManager.INTERFACE_NAME);
    }
    wifiService.start(wifiListener, interfaces, interfaces.length);
  } else {
    debug("No wifi service component available!");
  }

  // Callbacks to invoke when a reply arrives from the wifi service.
  var controlCallbacks = Object.create(null);
  var idgen = 0;

  function controlMessage(obj, callback) {
    var id = idgen++;
    obj.id = id;
    if (callback) {
      controlCallbacks[id] = callback;
    }
    wifiService.sendCommand(obj, obj.iface);
  }

  let onmessageresult = function(data, iface) {
    var id = data.id;
    var callback = controlCallbacks[id];
    if (callback) {
      callback(data);
      delete controlCallbacks[id];
    }
  }

  // Polling the status worker
  var recvErrors = 0;

  function waitForEvent(iface) {
    wifiService.waitForEvent(iface);
  }

  // Commands to the control worker.

  var driverLoaded = false;

  function loadDriver(callback) {
    if (driverLoaded) {
      callback(0);
      return;
    }

    wifiCommand.loadDriver(function (status) {
      driverLoaded = (status >= 0);
      callback(status)
    });
  }

  function unloadDriver(type, callback) {
    if (!unloadDriverEnabled) {
      // Unloading drivers is generally unnecessary and
      // can trigger bugs in some drivers.
      // On properly written drivers, bringing the interface
      // down powers down the interface.
      if (type === WIFI_FIRMWARE_STATION) {
        notify("supplicantlost", { success: true });
      }
      callback(0);
      return;
    }

    wifiCommand.unloadDriver(function(status) {
      driverLoaded = (status < 0);
      if (type === WIFI_FIRMWARE_STATION) {
        notify("supplicantlost", { success: true });
      }
      callback(status);
    });
  }

  // A note about background scanning:
  // Normally, background scanning shouldn't be necessary as wpa_supplicant
  // has the capability to automatically schedule its own scans at appropriate
  // intervals. However, with some drivers, this appears to get stuck after
  // three scans, so we enable the driver's background scanning to work around
  // that when we're not connected to any network. This ensures that we'll
  // automatically reconnect to networks if one falls out of range.
  var reEnableBackgroundScan = false;

  // NB: This is part of the internal API.
  manager.backgroundScanEnabled = false;
  function setBackgroundScan(enable, callback) {
    var doEnable = (enable === "ON");
    if (doEnable === manager.backgroundScanEnabled) {
      callback(false, true);
      return;
    }

    manager.backgroundScanEnabled = doEnable;
    wifiCommand.setBackgroundScan(manager.backgroundScanEnabled, callback);
  }

  var scanModeActive = false;

  function scan(forceActive, callback) {
    if (forceActive && !scanModeActive) {
      // Note: we ignore errors from doSetScanMode.
      wifiCommand.doSetScanMode(true, function(ignore) {
        setBackgroundScan("OFF", function(turned, ignore) {
          reEnableBackgroundScan = turned;
          manager.handlePreWifiScan();
          wifiCommand.scan(function(ok) {
            wifiCommand.doSetScanMode(false, function(ignore) {
              // The result of scanCommand is the result of the actual SCAN
              // request.
              callback(ok);
            });
          });
        });
      });
      return;
    }
    manager.handlePreWifiScan();
    wifiCommand.scan(callback);
  }

  var debugEnabled = false;

  function syncDebug() {
    if (debugEnabled !== DEBUG) {
      let wanted = DEBUG;
      wifiCommand.setLogLevel(wanted ? "DEBUG" : "INFO", function(ok) {
        if (ok)
          debugEnabled = wanted;
      });
      if (p2pSupported && p2pManager) {
        p2pManager.setDebug(DEBUG);
      }
    }
  }

  function getDebugEnabled(callback) {
    wifiCommand.getLogLevel(function(level) {
      if (level === null) {
        debug("Unable to get wpa_supplicant's log level");
        callback(false);
        return;
      }

      var lines = level.split("\n");
      for (let i = 0; i < lines.length; ++i) {
        let match = /Current level: (.*)/.exec(lines[i]);
        if (match) {
          debugEnabled = match[1].toLowerCase() === "debug";
          callback(true);
          return;
        }
      }

      // If we're here, we didn't get the current level.
      callback(false);
    });
  }

  function setScanMode(setActive, callback) {
    scanModeActive = setActive;
    wifiCommand.doSetScanMode(setActive, callback);
  }

  var httpProxyConfig = Object.create(null);

  /**
   * Given a network, configure http proxy when using wifi.
   * @param network A network object to update http proxy
   * @param info Info should have following field:
   *        - httpProxyHost ip address of http proxy.
   *        - httpProxyPort port of http proxy, set 0 to use default port 8080.
   * @param callback callback function.
   */
  function configureHttpProxy(network, info, callback) {
    if (!network)
      return;

    let networkKey = getNetworkKey(network);

    if (!info || info.httpProxyHost === "") {
      delete httpProxyConfig[networkKey];
    } else {
      httpProxyConfig[networkKey] = network;
      httpProxyConfig[networkKey].httpProxyHost = info.httpProxyHost;
      httpProxyConfig[networkKey].httpProxyPort = info.httpProxyPort;
    }

    callback(true);
  }

  function getHttpProxyNetwork(network) {
    if (!network)
      return null;

    let networkKey = getNetworkKey(network);
    return httpProxyConfig[networkKey];
  }

  function setHttpProxy(network) {
    if (!network)
      return;

    // If we got here, arg network must be the currentNetwork, so we just update
    // WifiNetworkInterface correspondingly and notify NetworkManager.
    WifiNetworkInterface.httpProxyHost = network.httpProxyHost;
    WifiNetworkInterface.httpProxyPort = network.httpProxyPort;

    if (WifiNetworkInterface.info.state ==
        Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) {
      gNetworkManager.updateNetworkInterface(WifiNetworkInterface);
    }
  }

  var staticIpConfig = Object.create(null);
  function setStaticIpMode(network, info, callback) {
    let setNetworkKey = getNetworkKey(network);
    let curNetworkKey = null;
    let currentNetwork = Object.create(null);
    currentNetwork.netId = manager.connectionInfo.id;

    manager.getNetworkConfiguration(currentNetwork, function () {
      curNetworkKey = getNetworkKey(currentNetwork);

      // Add additional information to static ip configuration
      // It is used to compatiable with information dhcp callback.
      info.ipaddr = netHelpers.stringToIP(info.ipaddr_str);
      info.gateway = netHelpers.stringToIP(info.gateway_str);
      info.mask_str = netHelpers.ipToString(netHelpers.makeMask(info.maskLength));

      // Optional
      info.dns1 = netHelpers.stringToIP(info.dns1_str);
      info.dns2 = netHelpers.stringToIP(info.dns2_str);
      info.proxy = netHelpers.stringToIP(info.proxy_str);

      staticIpConfig[setNetworkKey] = info;

      // If the ssid of current connection is the same as configured ssid
      // It means we need update current connection to use static IP address.
      if (setNetworkKey == curNetworkKey) {
        // Use configureInterface directly doesn't work, the network interface
        // and routing table is changed but still cannot connect to network
        // so the workaround here is disable interface the enable again to
        // trigger network reconnect with static ip.
        gNetworkService.disableInterface(manager.ifname, function (ok) {
          gNetworkService.enableInterface(manager.ifname, function (ok) {
            callback(ok);
          });
        });
        return;
      }

      callback(true);
    });
  }

  var dhcpInfo = null;

  function runStaticIp(ifname, key) {
    debug("Run static ip");

    // Read static ip information from settings.
    let staticIpInfo;

    if (!(key in staticIpConfig))
      return;

    staticIpInfo = staticIpConfig[key];

    // Stop dhcpd when use static IP
    if (dhcpInfo != null) {
      netUtil.stopDhcp(manager.ifname, function() {});
    }

    // Set ip, mask length, gateway, dns to network interface
    gNetworkService.configureInterface( { ifname: ifname,
                                          ipaddr: staticIpInfo.ipaddr,
                                          mask: staticIpInfo.maskLength,
                                          gateway: staticIpInfo.gateway,
                                          dns1: staticIpInfo.dns1,
                                          dns2: staticIpInfo.dns2 }, function (data) {
      netUtil.runIpConfig(ifname, staticIpInfo, function(data) {
        dhcpInfo = data.info;
        notify("networkconnected", data);
      });
    });
  }

  var suppressEvents = false;
  function notify(eventName, eventObject) {
    if (suppressEvents)
      return;
    var handler = manager["on" + eventName];
    if (handler) {
      if (!eventObject)
        eventObject = ({});
      handler.call(eventObject);
    }
  }

  function notifyStateChange(fields) {
    // If we're already in the COMPLETED state, we might receive events from
    // the supplicant that tell us that we're re-authenticating or reminding
    // us that we're associated to a network. In those cases, we don't need to
    // do anything, so just ignore them.
    if (manager.state === "COMPLETED" &&
        fields.state !== "DISCONNECTED" &&
        fields.state !== "INTERFACE_DISABLED" &&
        fields.state !== "INACTIVE" &&
        fields.state !== "SCANNING") {
      return false;
    }

    // Stop background scanning if we're trying to connect to a network.
    if (manager.backgroundScanEnabled &&
        (fields.state === "ASSOCIATING" ||
         fields.state === "ASSOCIATED" ||
         fields.state === "FOUR_WAY_HANDSHAKE" ||
         fields.state === "GROUP_HANDSHAKE" ||
         fields.state === "COMPLETED")) {
      setBackgroundScan("OFF", function() {});
    }

    fields.prevState = manager.state;
    // Detect wpa_supplicant's loop iterations.
    manager.supplicantLoopDetection(fields.prevState, fields.state);
    notify("statechange", fields);

    // Don't update state when and after disabling.
    if (manager.state === "DISABLING" ||
        manager.state === "UNINITIALIZED") {
      return false;
    }

    manager.state = fields.state;
    return true;
  }

  function parseStatus(status) {
    if (status === null) {
      debug("Unable to get wpa supplicant's status");
      return;
    }

    var ssid;
    var bssid;
    var state;
    var ip_address;
    var id;

    var lines = status.split("\n");
    for (let i = 0; i < lines.length; ++i) {
      let [key, value] = lines[i].split("=");
      switch (key) {
        case "wpa_state":
          state = value;
          break;
        case "ssid":
          ssid = value;
          break;
        case "bssid":
          bssid = value;
          break;
        case "ip_address":
          ip_address = value;
          break;
        case "id":
          id = value;
          break;
      }
    }

    if (bssid && ssid) {
      manager.connectionInfo.bssid = bssid;
      manager.connectionInfo.ssid = ssid;
      manager.connectionInfo.id = id;
    }

    if (ip_address)
      dhcpInfo = { ip_address: ip_address };

    notifyStateChange({ state: state, fromStatus: true });

    // If we parse the status and the supplicant has already entered the
    // COMPLETED state, then we need to set up DHCP right away.
    if (state === "COMPLETED")
      onconnected();
  }

  // try to connect to the supplicant
  var connectTries = 0;
  var retryTimer = null;
  function connectCallback(ok) {
    if (ok === 0) {
      // Tell the event worker to start waiting for events.
      retryTimer = null;
      connectTries = 0;
      recvErrors = 0;
      manager.connectToSupplicant = true;
      didConnectSupplicant(function(){});
      return;
    }
    if (connectTries++ < 5) {
      // Try again in 1 seconds.
      if (!retryTimer)
        retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);

      retryTimer.initWithCallback(function(timer) {
        wifiCommand.connectToSupplicant(connectCallback);
      }, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
      return;
    }

    retryTimer = null;
    connectTries = 0;
    notify("supplicantlost", { success: false });
  }

  manager.connectionDropped = function(callback) {
    // Reset network interface when connection drop
    gNetworkService.configureInterface( { ifname: manager.ifname,
                                          ipaddr: 0,
                                          mask: 0,
                                          gateway: 0,
                                          dns1: 0,
                                          dns2: 0 }, function (data) {
    });

    // If we got disconnected, kill the DHCP client in preparation for
    // reconnection.
    gNetworkService.resetConnections(manager.ifname, function() {
      netUtil.stopDhcp(manager.ifname, function() {
        callback();
      });
    });
  }

  manager.start = function() {
    debug("detected SDK version " + sdkVersion);
    wifiCommand.connectToSupplicant(connectCallback);
  }

  let dhcpRequestGen = 0;

  function onconnected() {
    // For now we do our own DHCP. In the future, this should be handed
    // off to the Network Manager.
    let currentNetwork = Object.create(null);
    currentNetwork.netId = manager.connectionInfo.id;

    manager.getNetworkConfiguration(currentNetwork, function (){
      let key = getNetworkKey(currentNetwork);
      if (staticIpConfig  &&
          (key in staticIpConfig) &&
          staticIpConfig[key].enabled) {
          debug("Run static ip");
          runStaticIp(manager.ifname, key);
          return;
      }
      netUtil.runDhcp(manager.ifname, dhcpRequestGen++, function(data, gen) {
        dhcpInfo = data.info;
        debug('dhcpRequestGen: ' + dhcpRequestGen + ', gen: ' + gen);
        if (!dhcpInfo) {
          if (gen + 1 < dhcpRequestGen) {
            debug('Do not bother younger DHCP request.');
            return;
          }
          if (++manager.dhcpFailuresCount >= MAX_RETRIES_ON_DHCP_FAILURE) {
            manager.dhcpFailuresCount = 0;
            notify("disconnected", {connectionInfo: manager.connectionInfo});
            return;
          }
          // NB: We have to call disconnect first. Otherwise, we only reauth with
          // the existing AP and don't retrigger DHCP.
          manager.disconnect(function() {
            manager.reassociate(function(){});
          });
          return;
        }

        manager.dhcpFailuresCount = 0;
        notify("networkconnected", data);
      });
    });
  }

  var supplicantStatesMap = (sdkVersion >= 15) ?
    ["DISCONNECTED", "INTERFACE_DISABLED", "INACTIVE", "SCANNING",
     "AUTHENTICATING", "ASSOCIATING", "ASSOCIATED", "FOUR_WAY_HANDSHAKE",
     "GROUP_HANDSHAKE", "COMPLETED"]
    :
    ["DISCONNECTED", "INACTIVE", "SCANNING", "ASSOCIATING",
     "ASSOCIATED", "FOUR_WAY_HANDSHAKE", "GROUP_HANDSHAKE",
     "COMPLETED", "DORMANT", "UNINITIALIZED"];

  var driverEventMap = { STOPPED: "driverstopped", STARTED: "driverstarted", HANGED: "driverhung" };

  manager.getNetworkId = function (ssid, callback) {
    manager.getConfiguredNetworks(function(networks) {
      if (!networks) {
        debug("Unable to get configured networks");
        return callback(null);
      }
      for (let net in networks) {
        let network = networks[net];
        // Trying to get netId from
        // 1. network matching SSID if SSID is provided.
        // 2. current network if no SSID is provided, it's not guaranteed that
        //    current network matches requested SSID.
        if ((!ssid && network.status === "CURRENT") ||
            (ssid && network.ssid && ssid === dequote(network.ssid))) {
          return callback(net);
        }
      }
      callback(null);
    });
  }

  function handleWpaEapEvents(event) {
    if (event.indexOf("CTRL-EVENT-EAP-FAILURE") !== -1) {
      if (event.indexOf("EAP authentication failed") !== -1) {
        notify("passwordmaybeincorrect");
        if (manager.authenticationFailuresCount > MAX_RETRIES_ON_AUTHENTICATION_FAILURE) {
          manager.authenticationFailuresCount = 0;
          notify("disconnected", {connectionInfo: manager.connectionInfo});
        }
      }
      return true;
    }
    if (event.indexOf("CTRL-EVENT-EAP-TLS-CERT-ERROR") !== -1) {
      // Cert Error
      notify("passwordmaybeincorrect");
      if (manager.authenticationFailuresCount > MAX_RETRIES_ON_AUTHENTICATION_FAILURE) {
        manager.authenticationFailuresCount = 0;
        notify("disconnected", {connectionInfo: manager.connectionInfo});
      }
      return true;
    }
    if (event.indexOf("CTRL-EVENT-EAP-STARTED") !== -1) {
      notifyStateChange({ state: "AUTHENTICATING" });
      return true;
    }
    return true;
  }

  // Handle events sent to us by the event worker.
  function handleEvent(event) {
    debug("Event coming in: " + event);
    if (event.indexOf("CTRL-EVENT-") !== 0 && event.indexOf("WPS") !== 0) {
      // Handle connection fail exception on WEP-128, while password length
      // is not 5 nor 13 bytes.
      if (event.indexOf("Association request to the driver failed") !== -1) {
        notify("passwordmaybeincorrect");
        if (manager.authenticationFailuresCount > MAX_RETRIES_ON_AUTHENTICATION_FAILURE) {
          manager.authenticationFailuresCount = 0;
          notify("disconnected", {connectionInfo: manager.connectionInfo});
        }
        return true;
      }

      if (event.indexOf("WPA:") == 0 &&
          event.indexOf("pre-shared key may be incorrect") != -1) {
        notify("passwordmaybeincorrect");
      }

      // This is ugly, but we need to grab the SSID here. BSSID is not guaranteed
      // to be provided, so don't grab BSSID here.
      var match = /Trying to associate with.*SSID[ =]'(.*)'/.exec(event);
      if (match) {
        debug("Matched: " + match[1] + "\n");
        manager.connectionInfo.ssid = match[1];
      }
      return true;
    }

    var space = event.indexOf(" ");
    var eventData = event.substr(0, space + 1);
    if (eventData.indexOf("CTRL-EVENT-STATE-CHANGE") === 0) {
      // Parse the event data.
      var fields = {};
      var tokens = event.substr(space + 1).split(" ");
      for (var n = 0; n < tokens.length; ++n) {
        var kv = tokens[n].split("=");
        if (kv.length === 2)
          fields[kv[0]] = kv[1];
      }
      if (!("state" in fields))
        return true;
      fields.state = supplicantStatesMap[fields.state];

      // The BSSID field is only valid in the ASSOCIATING and ASSOCIATED
      // states, except when we "reauth", except this seems to depend on the
      // driver, so simply check to make sure that we don't have a null BSSID.
      if (fields.BSSID !== "00:00:00:00:00:00")
        manager.connectionInfo.bssid = fields.BSSID;

      if (notifyStateChange(fields) && fields.state === "COMPLETED") {
        onconnected();
      }
      return true;
    }
    if (eventData.indexOf("CTRL-EVENT-DRIVER-STATE") === 0) {
      var handlerName = driverEventMap[eventData];
      if (handlerName)
        notify(handlerName);
      return true;
    }
    if (eventData.indexOf("CTRL-EVENT-TERMINATING") === 0) {
      // As long the monitor socket is not closed and we haven't seen too many
      // recv errors yet, we will keep going for a bit longer.
      if (event.indexOf("connection closed") === -1 &&
          event.indexOf("recv error") !== -1 && ++recvErrors < 10)
        return true;

      notifyStateChange({ state: "DISCONNECTED", BSSID: null, id: -1 });

      // If the supplicant is terminated as commanded, the supplicant lost
      // notification will be sent after driver unloaded. In such case, the
      // manager state will be "DISABLING" or "UNINITIALIZED".
      // So if supplicant terminated with incorrect manager state, implying
      // unexpected condition, we should notify supplicant lost here.
      if (manager.state !== "DISABLING" && manager.state !== "UNINITIALIZED") {
        notify("supplicantlost", { success: true });
      }

      if (manager.stopSupplicantCallback) {
        cancelWaitForTerminateEventTimer();
        // It's possible that the terminating event triggered by timer comes
        // earlier than the event from wpa_supplicant. Since
        // stopSupplicantCallback contains async. callbacks, swap it to local
        // to prevent calling the callback twice.
        let stopSupplicantCallback = manager.stopSupplicantCallback.bind(manager);
        manager.stopSupplicantCallback = null;
        stopSupplicantCallback();
        stopSupplicantCallback = null;
      }
      return false;
    }
    if (eventData.indexOf("CTRL-EVENT-DISCONNECTED") === 0) {
      var token = event.split(" ")[1];
      var bssid = token.split("=")[1];
      if (manager.authenticationFailuresCount > MAX_RETRIES_ON_AUTHENTICATION_FAILURE) {
        manager.authenticationFailuresCount = 0;
        notify("disconnected", {connectionInfo: manager.connectionInfo});
      }
      manager.connectionInfo.bssid = null;
      manager.connectionInfo.ssid = null;
      manager.connectionInfo.id = -1;
      return true;
    }
    if (eventData.indexOf("CTRL-EVENT-CONNECTED") === 0) {
      // Format: CTRL-EVENT-CONNECTED - Connection to 00:1e:58:ec:d5:6d completed (reauth) [id=1 id_str=]
      var bssid = event.split(" ")[4];

      var keyword = "id=";
      var id = event.substr(event.indexOf(keyword) + keyword.length).split(" ")[0];
      // Read current BSSID here, it will always being provided.
      manager.connectionInfo.id = id;
      manager.connectionInfo.bssid = bssid;
      return true;
    }
    if (eventData.indexOf("CTRL-EVENT-SCAN-RESULTS") === 0) {
      debug("Notifying of scan results available");
      if (reEnableBackgroundScan) {
        reEnableBackgroundScan = false;
        setBackgroundScan("ON", function() {});
      }
      manager.handlePostWifiScan();
      notify("scanresultsavailable");
      return true;
    }
    if (eventData.indexOf("CTRL-EVENT-EAP") === 0) {
      return handleWpaEapEvents(event);
    }
    if (eventData.indexOf("CTRL-EVENT-ASSOC-REJECT") === 0) {
      debug("CTRL-EVENT-ASSOC-REJECT: network error");
      notify("passwordmaybeincorrect");
      if (manager.authenticationFailuresCount > MAX_RETRIES_ON_AUTHENTICATION_FAILURE) {
        manager.authenticationFailuresCount = 0;
        debug("CTRL-EVENT-ASSOC-REJECT: disconnect network");
        notify("disconnected", {connectionInfo: manager.connectionInfo});
      }
      return true;
    }
    if (eventData.indexOf("WPS-TIMEOUT") === 0) {
      notifyStateChange({ state: "WPS_TIMEOUT", BSSID: null, id: -1 });
      return true;
    }
    if (eventData.indexOf("WPS-FAIL") === 0) {
      notifyStateChange({ state: "WPS_FAIL", BSSID: null, id: -1 });
      return true;
    }
    if (eventData.indexOf("WPS-OVERLAP-DETECTED") === 0) {
      notifyStateChange({ state: "WPS_OVERLAP_DETECTED", BSSID: null, id: -1 });
      return true;
    }
    // Unknown event.
    return true;
  }

  function setPowerSavingMode(enabled) {
    let mode = enabled ? "AUTO" : "ACTIVE";
    // Some wifi drivers may not implement this command. Set power mode
    // even if suspend optimization command failed.
    manager.setSuspendOptimizations(enabled, function(ok) {
      manager.setPowerMode(mode, function() {});
    });
  }

  function didConnectSupplicant(callback) {
    waitForEvent(manager.ifname);

    // Load up the supplicant state.
    getDebugEnabled(function(ok) {
      syncDebug();
    });
    wifiCommand.status(function(status) {
      parseStatus(status);
      notify("supplicantconnection");
      callback();
    });
    // WPA supplicant already connected.
    manager.setPowerSavingMode(true);
    if (p2pSupported) {
      manager.enableP2p(function(success) {});
    }
  }

  function prepareForStartup(callback) {
    let status = libcutils.property_get(DHCP_PROP + "_" + manager.ifname);
    if (status !== "running") {
      tryStopSupplicant();
      return;
    }
    manager.connectionDropped(function() {
      tryStopSupplicant();
    });

    // Ignore any errors and kill any currently-running supplicants. On some
    // phones, stopSupplicant won't work for a supplicant that we didn't
    // start, so we hand-roll it here.
    function tryStopSupplicant () {
      let status = libcutils.property_get(SUPP_PROP);
      if (status !== "running") {
        callback();
        return;
      }
      suppressEvents = true;
      wifiCommand.killSupplicant(function() {
        gNetworkService.disableInterface(manager.ifname, function (ok) {
          suppressEvents = false;
          callback();
        });
      });
    }
  }

  // Initial state.
  manager.state = "UNINITIALIZED";
  manager.tetheringState = "UNINITIALIZED";
  manager.supplicantStarted = false;
  manager.connectionInfo = { ssid: null, bssid: null, id: -1 };
  manager.authenticationFailuresCount = 0;
  manager.loopDetectionCount = 0;
  manager.dhcpFailuresCount = 0;
  manager.stopSupplicantCallback = null;

  manager.__defineGetter__("enabled", function() {
    switch (manager.state) {
      case "UNINITIALIZED":
      case "INITIALIZING":
      case "DISABLING":
        return false;
      default:
        return true;
    }
  });

  var waitForTerminateEventTimer = null;
  function cancelWaitForTerminateEventTimer() {
    if (waitForTerminateEventTimer) {
      waitForTerminateEventTimer.cancel();
      waitForTerminateEventTimer = null;
    }
  };
  function createWaitForTerminateEventTimer(onTimeout) {
    if (waitForTerminateEventTimer) {
      return;
    }
    waitForTerminateEventTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    waitForTerminateEventTimer.initWithCallback(onTimeout,
                                                4000,
                                                Ci.nsITimer.TYPE_ONE_SHOT);
  };

  var waitForDriverReadyTimer = null;
  function cancelWaitForDriverReadyTimer() {
    if (waitForDriverReadyTimer) {
      waitForDriverReadyTimer.cancel();
      waitForDriverReadyTimer = null;
    }
  };
  function createWaitForDriverReadyTimer(onTimeout) {
    waitForDriverReadyTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    waitForDriverReadyTimer.initWithCallback(onTimeout,
                                             manager.driverDelay,
                                             Ci.nsITimer.TYPE_ONE_SHOT);
  };

  // Public interface of the wifi service.
  manager.setWifiEnabled = function(enabled, callback) {
    if (enabled === manager.isWifiEnabled(manager.state)) {
      callback("no change");
      return;
    }

    if (enabled) {
      manager.state = "INITIALIZING";
      // Register as network interface.
      WifiNetworkInterface.info.name = manager.ifname;
      if (!WifiNetworkInterface.registered) {
        gNetworkManager.registerNetworkInterface(WifiNetworkInterface);
        WifiNetworkInterface.registered = true;
      }
      WifiNetworkInterface.info.state =
        Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED;
      WifiNetworkInterface.info.ips = [];
      WifiNetworkInterface.info.prefixLengths = [];
      WifiNetworkInterface.info.gateways = [];
      WifiNetworkInterface.info.dnses = [];
      gNetworkManager.updateNetworkInterface(WifiNetworkInterface);

      prepareForStartup(function() {
        loadDriver(function (status) {
          if (status < 0) {
            callback(status);
            manager.state = "UNINITIALIZED";
            return;
          }
          // This command is mandatory for Nexus 4. But some devices like
          // Galaxy S2 don't support it. Continue to start wpa_supplicant
          // even if we fail to set wifi operation mode to station.
          gNetworkService.setWifiOperationMode(manager.ifname,
                                               WIFI_FIRMWARE_STATION,
                                               function (status) {

            function startSupplicantInternal() {
              wifiCommand.startSupplicant(function (status) {
                if (status < 0) {
                  unloadDriver(WIFI_FIRMWARE_STATION, function() {
                    callback(status);
                  });
                  manager.state = "UNINITIALIZED";
                  return;
                }

                manager.supplicantStarted = true;
                gNetworkService.enableInterface(manager.ifname, function (ok) {
                  callback(ok ? 0 : -1);
                });
              });
            }

            function doStartSupplicant() {
              cancelWaitForDriverReadyTimer();

              if (!manager.connectToSupplicant) {
                startSupplicantInternal();
                return;
              }
              wifiCommand.closeSupplicantConnection(function () {
                manager.connectToSupplicant = false;
                // closeSupplicantConnection() will trigger onsupplicantlost
                // and set manager.state to "UNINITIALIZED", we have to
                // restore it here.
                manager.state = "INITIALIZING";
                startSupplicantInternal();
              });
            }
            // Driver startup on certain platforms takes longer than it takes for us
            // to return from loadDriver, so wait 2 seconds before starting
            // the supplicant to give it a chance to start.
            if (manager.driverDelay > 0) {
              createWaitForDriverReadyTimer(doStartSupplicant);
            } else {
              doStartSupplicant();
            }
          });
        });
      });
    } else {
      manager.state = "DISABLING";
      // Note these following calls ignore errors. If we fail to kill the
      // supplicant gracefully, then we need to continue telling it to die
      // until it does.
      let doDisableWifi = function() {
        manager.stopSupplicantCallback = (function () {
          wifiCommand.stopSupplicant(function (status) {
            wifiCommand.closeSupplicantConnection(function() {
              manager.connectToSupplicant = false;
              manager.state = "UNINITIALIZED";
              gNetworkService.disableInterface(manager.ifname, function (ok) {
                unloadDriver(WIFI_FIRMWARE_STATION, callback);
              });
            });
          });
        }).bind(this);

        let terminateEventCallback = (function() {
          handleEvent("CTRL-EVENT-TERMINATING");
        }).bind(this);
        createWaitForTerminateEventTimer(terminateEventCallback);

        // We are going to terminate the connection between wpa_supplicant.
        // Stop the polling timer immediately to prevent connection info update
        // command blocking in control thread until socket timeout.
        notify("stopconnectioninfotimer");

        wifiCommand.terminateSupplicant(function (ok) {
          manager.connectionDropped(function () {
          });
        });
      }

      if (p2pSupported) {
        p2pManager.setEnabled(false, { onDisabled: doDisableWifi });
      } else {
        doDisableWifi();
      }
    }
  }

  var wifiHotspotStatusTimer = null;
  function cancelWifiHotspotStatusTimer() {
    if (wifiHotspotStatusTimer) {
      wifiHotspotStatusTimer.cancel();
      wifiHotspotStatusTimer = null;
    }
  }

  function createWifiHotspotStatusTimer(onTimeout) {
    wifiHotspotStatusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    wifiHotspotStatusTimer.init(onTimeout, 5000, Ci.nsITimer.TYPE_REPEATING_SLACK);
  }

  // Get wifi interface and load wifi driver when enable Ap mode.
  manager.setWifiApEnabled = function(enabled, configuration, callback) {
    if (enabled === manager.isWifiTetheringEnabled(manager.tetheringState)) {
      callback("no change");
      return;
    }

    if (enabled) {
      manager.tetheringState = "INITIALIZING";
      loadDriver(function (status) {
        if (status < 0) {
          callback();
          manager.tetheringState = "UNINITIALIZED";
          if (wifiHotspotStatusTimer) {
            cancelWifiHotspotStatusTimer();
            wifiCommand.closeHostapdConnection(function(result) {
            });
          }
          return;
        }

        function getWifiHotspotStatus() {
          wifiCommand.hostapdGetStations(function(result) {
            notify("stationinfoupdate", {station: result});
          });
        }

        function doStartWifiTethering() {
          cancelWaitForDriverReadyTimer();
          WifiNetworkInterface.info.name =
            libcutils.property_get("wifi.tethering.interface", manager.ifname);
          gTetheringService.setWifiTethering(enabled,
                                             WifiNetworkInterface.info.name,
                                             configuration, function(result) {
            if (result) {
              manager.tetheringState = "UNINITIALIZED";
            } else {
              manager.tetheringState = "COMPLETED";
              wifiCommand.connectToHostapd(function(result) {
                if (result) {
                  return;
                }
                // Create a timer to track the connection status.
                createWifiHotspotStatusTimer(getWifiHotspotStatus);
              });
            }
            // Pop out current request.
            callback();
            // Should we fire a dom event if we fail to set wifi tethering  ?
            debug("Enable Wifi tethering result: " + (result ? result : "successfully"));
          });
        }

        // Driver startup on certain platforms takes longer than it takes
        // for us to return from loadDriver, so wait 2 seconds before
        // turning on Wifi tethering.
        createWaitForDriverReadyTimer(doStartWifiTethering);
      });
    } else {
      cancelWifiHotspotStatusTimer();
      gTetheringService.setWifiTethering(enabled, WifiNetworkInterface,
                                         configuration, function(result) {
        // Should we fire a dom event if we fail to set wifi tethering  ?
        debug("Disable Wifi tethering result: " + (result ? result : "successfully"));
        // Unload wifi driver even if we fail to control wifi tethering.
        unloadDriver(WIFI_FIRMWARE_AP, function(status) {
          if (status < 0) {
            debug("Fail to unload wifi driver");
          }
          manager.tetheringState = "UNINITIALIZED";
          callback();
        });
      });
    }
  }

  manager.disconnect = wifiCommand.disconnect;
  manager.reconnect = wifiCommand.reconnect;
  manager.reassociate = wifiCommand.reassociate;

  var networkConfigurationFields = [
    {name: "ssid",          type: "string"},
    {name: "bssid",         type: "string"},
    {name: "psk",           type: "string"},
    {name: "wep_key0",      type: "string"},
    {name: "wep_key1",      type: "string"},
    {name: "wep_key2",      type: "string"},
    {name: "wep_key3",      type: "string"},
    {name: "wep_tx_keyidx", type: "integer"},
    {name: "priority",      type: "integer"},
    {name: "key_mgmt",      type: "string"},
    {name: "scan_ssid",     type: "string"},
    {name: "disabled",      type: "integer"},
    {name: "identity",      type: "string"},
    {name: "password",      type: "string"},
    {name: "auth_alg",      type: "string"},
    {name: "phase1",        type: "string"},
    {name: "phase2",        type: "string"},
    {name: "eap",           type: "string"},
    {name: "pin",           type: "string"},
    {name: "pcsc",          type: "string"},
    {name: "ca_cert",       type: "string"},
    {name: "subject_match", type: "string"},
    {name: "client_cert",   type: "string"},
    {name: "private_key",   type: "stirng"},
    {name: "engine",        type: "integer"},
    {name: "engine_id",     type: "string"},
    {name: "key_id",        type: "string"},
    {name: "frequency",     type: "integer"},
    {name: "mode",          type: "integer"}
  ];
  // These fields are only handled in IBSS (aka ad-hoc) mode
  var ibssNetworkConfigurationFields = [
    "frequency", "mode"
  ];

  manager.getNetworkConfiguration = function(config, callback) {
    var netId = config.netId;
    var done = 0;
    for (var n = 0; n < networkConfigurationFields.length; ++n) {
      let fieldName = networkConfigurationFields[n].name;
      let fieldType = networkConfigurationFields[n].type;
      wifiCommand.getNetworkVariable(netId, fieldName, function(value) {
        if (value !== null) {
          if (fieldType === "integer") {
            config[fieldName] = parseInt(value, 10);
          } else if ( fieldName == "ssid" && value[0] != '"' ) {
            // SET_NETWORK will set a quoted ssid to wpa_supplicant.
            // But if ssid contains non-ascii char, it will be converted into utf-8.
            // For example: "Test的wifi" --> 54657374e79a8477696669
            // When GET_NETWORK receive a un-quoted utf-8 ssid, it must be decoded and quoted.
            config[fieldName] = quote(decodeURIComponent(value.replace(/[0-9a-f]{2}/g, '%$&')));
          } else {
            // value is string type by default.
            config[fieldName] = value;
          }
        }
        if (++done == networkConfigurationFields.length)
          callback(config);
      });
    }
  }
  manager.setNetworkConfiguration = function(config, callback) {
    var netId = config.netId;
    var done = 0;
    var errors = 0;

    function hasValidProperty(name) {
      return ((name in config) &&
               config[name] != null &&
               (["password", "wep_key0", "psk"].indexOf(name) === -1 ||
                config[name] !== '*'));
    }

    function getModeFromConfig() {
      /* we use the mode from the config, or ESS as default */
      return hasValidProperty("mode") ? config["mode"] : MODE_ESS;
    }

    var mode = getModeFromConfig();

    function validForMode(name, mode) {
            /* all fields are valid for IBSS */
      return (mode == MODE_IBSS) ||
            /* IBSS fields are not valid for ESS */
            ((mode == MODE_ESS) && !(name in ibssNetworkConfigurationFields));
    }

    for (var n = 0; n < networkConfigurationFields.length; ++n) {
      let fieldName = networkConfigurationFields[n].name;
      if (!hasValidProperty(fieldName) || !validForMode(fieldName, mode)) {
        ++done;
      } else {
        wifiCommand.setNetworkVariable(netId, fieldName, config[fieldName], function(ok) {
          if (!ok)
            ++errors;
          if (++done == networkConfigurationFields.length)
            callback(errors == 0);
        });
      }
    }
    // If config didn't contain any of the fields we want, don't lose the error callback.
    if (done == networkConfigurationFields.length)
      callback(false);
  }
  manager.getConfiguredNetworks = function(callback) {
    wifiCommand.listNetworks(function (reply) {
      var networks = Object.create(null);
      var lines = reply ? reply.split("\n") : 0;
      if (lines.length <= 1) {
        // We need to make sure we call the callback even if there are no
        // configured networks.
        callback(networks);
        return;
      }

      var done = 0;
      var errors = 0;
      for (var n = 1; n < lines.length; ++n) {
        var result = lines[n].split("\t");
        var netId = parseInt(result[0], 10);
        var config = networks[netId] = { netId: netId };
        switch (result[3]) {
        case "[CURRENT]":
          config.status = "CURRENT";
          break;
        case "[DISABLED]":
          config.status = "DISABLED";
          break;
        default:
          config.status = "ENABLED";
          break;
        }
        manager.getNetworkConfiguration(config, function (ok) {
            if (!ok)
              ++errors;
            if (++done == lines.length - 1) {
              if (errors) {
                // If an error occured, delete the new netId.
                wifiCommand.removeNetwork(netId, function() {
                  callback(null);
                });
              } else {
                callback(networks);
              }
            }
        });
      }
    });
  }
  manager.addNetwork = function(config, callback) {
    wifiCommand.addNetwork(function (netId) {
      config.netId = netId;
      manager.setNetworkConfiguration(config, function (ok) {
        if (!ok) {
          wifiCommand.removeNetwork(netId, function() { callback(false); });
          return;
        }

        callback(ok);
      });
    });
  }
  manager.updateNetwork = function(config, callback) {
    manager.setNetworkConfiguration(config, callback);
  }
  manager.removeNetwork = function(netId, callback) {
    wifiCommand.removeNetwork(netId, callback);
  }

  manager.saveConfig = function(callback) {
    wifiCommand.saveConfig(callback);
  }
  manager.enableNetwork = function(netId, disableOthers, callback) {
    if (p2pSupported) {
      // We have to stop wifi direct scan before associating to an AP.
      // Otherwise we will get a "REJECT" wpa supplicant event.
      p2pManager.setScanEnabled(false, function(success) {});
    }
    wifiCommand.enableNetwork(netId, disableOthers, callback);
  }
  manager.disableNetwork = function(netId, callback) {
    wifiCommand.disableNetwork(netId, callback);
  }
  manager.getMacAddress = wifiCommand.getMacAddress;
  manager.getScanResults = wifiCommand.scanResults;
  manager.setScanMode = function(mode, callback) {
    setScanMode(mode === "active", callback); // Use our own version.
  }
  manager.setBackgroundScan = setBackgroundScan; // Use our own version.
  manager.scan = scan; // Use our own version.
  manager.wpsPbc = wifiCommand.wpsPbc;
  manager.wpsPin = wifiCommand.wpsPin;
  manager.wpsCancel = wifiCommand.wpsCancel;
  manager.setPowerMode = (sdkVersion >= 16)
                         ? wifiCommand.setPowerModeJB
                         : wifiCommand.setPowerModeICS;
  manager.setPowerSavingMode = setPowerSavingMode;
  manager.getHttpProxyNetwork = getHttpProxyNetwork;
  manager.setHttpProxy = setHttpProxy;
  manager.configureHttpProxy = configureHttpProxy;
  manager.setSuspendOptimizations = (sdkVersion >= 16)
                                   ? wifiCommand.setSuspendOptimizationsJB
                                   : wifiCommand.setSuspendOptimizationsICS;
  manager.setStaticIpMode = setStaticIpMode;
  manager.getRssiApprox = wifiCommand.getRssiApprox;
  manager.getLinkSpeed = wifiCommand.getLinkSpeed;
  manager.getDhcpInfo = function() { return dhcpInfo; }
  manager.getConnectionInfo = (sdkVersion >= 15)
                              ? wifiCommand.getConnectionInfoICS
                              : wifiCommand.getConnectionInfoGB;

  manager.ensureSupplicantDetached = aCallback => {
    if (!manager.enabled) {
      aCallback();
      return;
    }
    wifiCommand.closeSupplicantConnection(aCallback);
  };

  manager.isHandShakeState = function(state) {
    switch (state) {
      case "AUTHENTICATING":
      case "ASSOCIATING":
      case "ASSOCIATED":
      case "FOUR_WAY_HANDSHAKE":
      case "GROUP_HANDSHAKE":
        return true;
      case "DORMANT":
      case "COMPLETED":
      case "DISCONNECTED":
      case "INTERFACE_DISABLED":
      case "INACTIVE":
      case "SCANNING":
      case "UNINITIALIZED":
      case "INVALID":
      case "CONNECTED":
      default:
        return false;
    }
  }

  manager.isWifiEnabled = function(state) {
    switch (state) {
      case "UNINITIALIZED":
      case "DISABLING":
        return false;
      default:
        return true;
    }
  }

  manager.isWifiTetheringEnabled = function(state) {
    switch (state) {
      case "UNINITIALIZED":
        return false;
      default:
        return true;
    }
  }

  manager.syncDebug = syncDebug;
  manager.stateOrdinal = function(state) {
    return supplicantStatesMap.indexOf(state);
  }
  manager.supplicantLoopDetection = function(prevState, state) {
    var isPrevStateInHandShake = manager.isHandShakeState(prevState);
    var isStateInHandShake = manager.isHandShakeState(state);

    if (isPrevStateInHandShake) {
      if (isStateInHandShake) {
        // Increase the count only if we are in the loop.
        if (manager.stateOrdinal(state) > manager.stateOrdinal(prevState)) {
          manager.loopDetectionCount++;
        }
        if (manager.loopDetectionCount > MAX_SUPPLICANT_LOOP_ITERATIONS) {
          notify("disconnected", {connectionInfo: manager.connectionInfo});
          manager.loopDetectionCount = 0;
        }
      }
    } else {
      // From others state to HandShake state. Reset the count.
      if (isStateInHandShake) {
        manager.loopDetectionCount = 0;
      }
    }
  }

  manager.handlePreWifiScan = function() {
    if (p2pSupported) {
      // Before doing regular wifi scan, we have to disable wifi direct
      // scan first. Otherwise we will never get the scan result.
      p2pManager.blockScan();
    }
  };

  manager.handlePostWifiScan = function() {
    if (p2pSupported) {
      // After regular wifi scanning, we should restore the restricted
      // wifi direct scan.
      p2pManager.unblockScan();
    }
  };

  //
  // Public APIs for P2P.
  //

  manager.p2pSupported = function() {
    return p2pSupported;
  };

  manager.getP2pManager = function() {
    return p2pManager;
  };

  manager.enableP2p = function(callback) {
    p2pManager.setEnabled(true, {
      onSupplicantConnected: function() {
        waitForEvent(WifiP2pManager.INTERFACE_NAME);
      },

      onEnabled: function(success) {
        callback(success);
      }
    });
  };

  manager.getCapabilities = function() {
    return capabilities;
  }

  // Cert Services
  let wifiCertService = Cc["@mozilla.org/wifi/certservice;1"];
  if (wifiCertService) {
    wifiCertService = wifiCertService.getService(Ci.nsIWifiCertService);
    wifiCertService.start(wifiListener);
  } else {
    debug("No wifi CA service component available");
  }

  manager.importCert = function(caInfo, callback) {
    var id = idgen++;
    if (callback) {
      controlCallbacks[id] = callback;
    }

    wifiCertService.importCert(id, caInfo.certBlob, caInfo.certPassword,
                               caInfo.certNickname);
  }

  manager.deleteCert = function(caInfo, callback) {
    var id = idgen++;
    if (callback) {
      controlCallbacks[id] = callback;
    }

    wifiCertService.deleteCert(id, caInfo.certNickname);
  }

  manager.sdkVersion = function() {
    return sdkVersion;
  }

  return manager;
})();

// Get unique key for a network, now the key is created by escape(SSID)+Security.
// So networks of same SSID but different security mode can be identified.
function getNetworkKey(network)
{
  var ssid = "",
      encryption = "OPEN";

  if ("security" in network) {
    // manager network object, represents an AP
    // object structure
    // {
    //   .ssid           : SSID of AP
    //   .security[]     : "WPA-PSK" for WPA-PSK
    //                     "WPA-EAP" for WPA-EAP
    //                     "WEP" for WEP
    //                     "" for OPEN
    //   other keys
    // }

    var security = network.security;
    ssid = network.ssid;

    for (let j = 0; j < security.length; j++) {
      if (security[j] === "WPA-PSK") {
        encryption = "WPA-PSK";
        break;
      } else if (security[j] === "WPA-EAP") {
        encryption = "WPA-EAP";
        break;
      } else if (security[j] === "WEP") {
        encryption = "WEP";
        break;
      }
    }
  } else if ("key_mgmt" in network) {
    // configure network object, represents a network
    // object structure
    // {
    //   .ssid           : SSID of network, quoted
    //   .key_mgmt       : Encryption type
    //                     "WPA-PSK" for WPA-PSK
    //                     "WPA-EAP" for WPA-EAP
    //                     "NONE" for WEP/OPEN
    //   .auth_alg       : Encryption algorithm(WEP mode only)
    //                     "OPEN_SHARED" for WEP
    //   other keys
    // }
    var key_mgmt = network.key_mgmt,
        auth_alg = network.auth_alg;
    ssid = dequote(network.ssid);

    if (key_mgmt == "WPA-PSK") {
      encryption = "WPA-PSK";
    } else if (key_mgmt.indexOf("WPA-EAP") != -1) {
      encryption = "WPA-EAP";
    } else if (key_mgmt == "NONE" && auth_alg === "OPEN SHARED") {
      encryption = "WEP";
    }
  }

  // ssid here must be dequoted, and it's safer to esacpe it.
  // encryption won't be empty and always be assigned one of the followings :
  // "OPEN"/"WEP"/"WPA-PSK"/"WPA-EAP".
  // So for a invalid network object, the returned key will be "OPEN".
  return escape(ssid) + encryption;
}

function getMode(flags) {
  if (/\[IBSS/.test(flags))
    return MODE_IBSS;

  return MODE_ESS;
}

function getKeyManagement(flags) {
  var types = [];
  if (!flags)
    return types;

  if (/\[WPA2?-PSK/.test(flags))
    types.push("WPA-PSK");
  if (/\[WPA2?-EAP/.test(flags))
    types.push("WPA-EAP");
  if (/\[WEP/.test(flags))
    types.push("WEP");
  return types;
}

function getCapabilities(flags) {
  var types = [];
  if (!flags)
    return types;

  if (/\[WPS/.test(flags))
    types.push("WPS");
  return types;
}

// These constants shamelessly ripped from WifiManager.java
// strength is the value returned by scan_results. It is nominally in dB. We
// transform it into a percentage for clients looking to simply show a
// relative indication of the strength of a network.
const MIN_RSSI = -100;
const MAX_RSSI = -55;

function calculateSignal(strength) {
  // Some wifi drivers represent their signal strengths as 8-bit integers, so
  // in order to avoid negative numbers, they add 256 to the actual values.
  // While we don't *know* that this is the case here, we make an educated
  // guess.
  if (strength > 0)
    strength -= 256;

  if (strength <= MIN_RSSI)
    return 0;
  if (strength >= MAX_RSSI)
    return 100;
  return Math.floor(((strength - MIN_RSSI) / (MAX_RSSI - MIN_RSSI)) * 100);
}

function Network(ssid, mode, frequency, security, password, capabilities) {
  this.ssid = ssid;
  this.mode = mode;
  this.frequency = frequency;
  this.security = security;

  if (typeof password !== "undefined")
    this.password = password;
  if (capabilities !== undefined)
    this.capabilities = capabilities;
  // TODO connected here as well?

  this.__exposedProps__ = Network.api;
}

Network.api = {
  ssid: "r",
  mode: "r",
  frequency: "r",
  security: "r",
  capabilities: "r",
  known: "r",

  password: "rw",
  keyManagement: "rw",
  psk: "rw",
  identity: "rw",
  wep: "rw",
  hidden: "rw",
  eap: "rw",
  pin: "rw",
  phase1: "rw",
  phase2: "rw",
  serverCertificate: "rw",
  userCertificate: "rw"
};

// Note: We never use ScanResult.prototype, so the fact that it's unrelated to
// Network.prototype is OK.
function ScanResult(ssid, bssid, frequency, flags, signal) {
  Network.call(this, ssid, getMode(flags), frequency,
               getKeyManagement(flags), undefined, getCapabilities(flags));
  this.bssid = bssid;
  this.signalStrength = signal;
  this.relSignalStrength = calculateSignal(Number(signal));

  this.__exposedProps__ = ScanResult.api;
}

// XXX This should probably live in the DOM-facing side, but it's hard to do
// there, so we stick this here.
ScanResult.api = {
  bssid: "r",
  signalStrength: "r",
  relSignalStrength: "r",
  connected: "r"
};

for (let i in Network.api) {
  ScanResult.api[i] = Network.api[i];
}

function quote(s) {
  return '"' + s + '"';
}

function dequote(s) {
  if (s[0] != '"' || s[s.length - 1] != '"')
    throw "Invalid argument, not a quoted string: " + s;
  return s.substr(1, s.length - 2);
}

function isWepHexKey(s) {
  if (s.length != 10 && s.length != 26 && s.length != 58)
    return false;
  return !/[^a-fA-F0-9]/.test(s);
}


var WifiNetworkInterface = {

  QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface]),

  registered: false,

  // nsINetworkInterface

  NETWORK_STATE_UNKNOWN:       Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN,
  NETWORK_STATE_CONNECTING:    Ci.nsINetworkInfo.CONNECTING,
  NETWORK_STATE_CONNECTED:     Ci.nsINetworkInfo.CONNECTED,
  NETWORK_STATE_DISCONNECTING: Ci.nsINetworkInfo.DISCONNECTING,
  NETWORK_STATE_DISCONNECTED:  Ci.nsINetworkInfo.DISCONNECTED,

  NETWORK_TYPE_WIFI:        Ci.nsINetworkInfo.NETWORK_TYPE_WIFI,
  NETWORK_TYPE_MOBILE:      Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE,
  NETWORK_TYPE_MOBILE_MMS:  Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS,
  NETWORK_TYPE_MOBILE_SUPL: Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL,

  info: {
    QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo]),

    state: Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN,

    type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI,

    name: null,

    ips: [],

    prefixLengths: [],

    dnses: [],

    gateways: [],

    getAddresses: function (ips, prefixLengths) {
      ips.value = this.ips.slice();
      prefixLengths.value = this.prefixLengths.slice();

      return this.ips.length;
    },

    getGateways: function (count) {
      if (count) {
        count.value = this.gateways.length;
      }
      return this.gateways.slice();
    },

    getDnses: function (count) {
      if (count) {
        count.value = this.dnses.length;
      }
      return this.dnses.slice();
    }
  },

  httpProxyHost: null,

  httpProxyPort: null
};

function WifiScanResult() {}

// TODO Make the difference between a DOM-based network object and our
// networks objects much clearer.
var netToDOM;
var netFromDOM;

function WifiWorker() {
  var self = this;

  this._mm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
               .getService(Ci.nsIMessageListenerManager);
  const messages = ["WifiManager:getNetworks", "WifiManager:getKnownNetworks",
                    "WifiManager:associate", "WifiManager:forget",
                    "WifiManager:wps", "WifiManager:getState",
                    "WifiManager:setPowerSavingMode",
                    "WifiManager:setHttpProxy",
                    "WifiManager:setStaticIpMode",
                    "WifiManager:importCert",
                    "WifiManager:getImportedCerts",
                    "WifiManager:deleteCert",
                    "WifiManager:setWifiEnabled",
                    "WifiManager:setWifiTethering",
                    "child-process-shutdown"];

  messages.forEach((function(msgName) {
    this._mm.addMessageListener(msgName, this);
  }).bind(this));

  Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false);
  Services.obs.addObserver(this, "xpcom-shutdown", false);

  this.wantScanResults = [];

  this._needToEnableNetworks = false;
  this._highestPriority = -1;

  // Networks is a map from SSID -> a scan result.
  this.networks = Object.create(null);

  // ConfiguredNetworks is a map from SSID -> our view of a network. It only
  // lists networks known to the wpa_supplicant. The SSID field (and other
  // fields) are quoted for ease of use with WifiManager commands.
  // Note that we don't have to worry about escaping embedded quotes since in
  // all cases, the supplicant will take the last quotation that we pass it as
  // the end of the string.
  this.configuredNetworks = Object.create(null);
  this._addingNetworks = Object.create(null);

  this.currentNetwork = null;
  this.ipAddress = "";
  this.macAddress = null;

  this._lastConnectionInfo = null;
  this._connectionInfoTimer = null;
  this._reconnectOnDisconnect = false;

  // Create p2pObserver and assign to p2pManager.
  if (WifiManager.p2pSupported()) {
    this._p2pObserver = WifiP2pWorkerObserver(WifiManager.getP2pManager());
    WifiManager.getP2pManager().setObserver(this._p2pObserver);

    // Add DOM message observerd by p2pObserver to the message listener as well.
    this._p2pObserver.getObservedDOMMessages().forEach((function(msgName) {
      this._mm.addMessageListener(msgName, this);
    }).bind(this));
  }

  // Users of instances of nsITimer should keep a reference to the timer until
  // it is no longer needed in order to assure the timer is fired.
  this._callbackTimer = null;

  // XXX On some phones (Otoro and Unagi) the wifi driver doesn't play nicely
  // with the automatic scans that wpa_supplicant does (it appears that the
  // driver forgets that it's returned scan results and then refuses to try to
  // rescan. In order to detect this case we start a timer when we enter the
  // SCANNING state and reset it whenever we either get scan results or leave
  // the SCANNING state. If the timer fires, we assume that we are stuck and
  // forceably try to unstick the supplican, also turning on background
  // scanning to avoid having to constantly poke the supplicant.

  // How long we wait is controlled by the SCAN_STUCK_WAIT constant.
  const SCAN_STUCK_WAIT = 12000;
  this._scanStuckTimer = null;
  this._turnOnBackgroundScan = false;

  function startScanStuckTimer() {
    if (WifiManager.schedScanRecovery) {
      self._scanStuckTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
      self._scanStuckTimer.initWithCallback(scanIsStuck, SCAN_STUCK_WAIT,
                                            Ci.nsITimer.TYPE_ONE_SHOT);
    }
  }

  function scanIsStuck() {
    // Uh-oh, we've waited too long for scan results. Disconnect (which
    // guarantees that we leave the SCANNING state and tells wpa_supplicant to
    // wait for our next command) ensure that background scanning is on and
    // then try again.
    debug("Determined that scanning is stuck, turning on background scanning!");
    WifiManager.handlePostWifiScan();
    WifiManager.disconnect(function(ok) {});
    self._turnOnBackgroundScan = true;
  }

  // A list of requests to turn wifi on or off.
  this._stateRequests = [];

  // Given a connection status network, takes a network from
  // self.configuredNetworks and prepares it for the DOM.
  netToDOM = function(net) {
    if (!net) {
      return null;
    }
    var ssid = dequote(net.ssid);
    var mode = net.mode;
    var frequency = net.frequency;
    var security = (net.key_mgmt === "NONE" && net.wep_key0) ? ["WEP"] :
                   (net.key_mgmt && net.key_mgmt !== "NONE") ? [net.key_mgmt.split(" ")[0]] :
                   [];
    var password;
    if (("psk" in net && net.psk) ||
        ("password" in net && net.password) ||
        ("wep_key0" in net && net.wep_key0)) {
      password = "*";
    }

    var pub = new Network(ssid, mode, frequency, security, password);
    if (net.identity)
      pub.identity = dequote(net.identity);
    if ("netId" in net)
      pub.known = true;
    if (net.scan_ssid === 1)
      pub.hidden = true;
    if ("ca_cert" in net && net.ca_cert &&
        net.ca_cert.indexOf("keystore://WIFI_SERVERCERT_" === 0)) {
      pub.serverCertificate = net.ca_cert.substr(27);
    }
    if(net.subject_match) {
      pub.subjectMatch = net.subject_match;
    }
    if ("client_cert" in net && net.client_cert &&
        net.client_cert.indexOf("keystore://WIFI_USERCERT_" === 0)) {
      pub.userCertificate = net.client_cert.substr(25);
    }
    return pub;
  };

  netFromDOM = function(net, configured) {
    // Takes a network from the DOM and makes it suitable for insertion into
    // self.configuredNetworks (that is calling addNetwork will do the right
    // thing).
    // NB: Modifies net in place: safe since we don't share objects between
    // the dom and the chrome code.

    // Things that are useful for the UI but not to us.
    delete net.bssid;
    delete net.signalStrength;
    delete net.relSignalStrength;
    delete net.security;
    delete net.capabilities;

    if (!configured)
      configured = {};

    net.ssid = quote(net.ssid);

    let wep = false;
    if ("keyManagement" in net) {
      if (net.keyManagement === "WEP") {
        wep = true;
        net.keyManagement = "NONE";
      } else if (net.keyManagement === "WPA-EAP") {
        net.keyManagement += " IEEE8021X";
      }

      configured.key_mgmt = net.key_mgmt = net.keyManagement; // WPA2-PSK, WPA-PSK, etc.
      delete net.keyManagement;
    } else {
      configured.key_mgmt = net.key_mgmt = "NONE";
    }

    if (net.hidden) {
      configured.scan_ssid = net.scan_ssid = 1;
      delete net.hidden;
    }

    function checkAssign(name, checkStar) {
      if (name in net) {
        let value = net[name];
        if (!value || (checkStar && value === '*')) {
          if (name in configured)
            net[name] = configured[name];
          else
            delete net[name];
        } else {
          configured[name] = net[name] = quote(value);
        }
      }
    }

    checkAssign("psk", true);
    checkAssign("identity", false);
    checkAssign("password", true);
    if (wep && net.wep && net.wep != '*') {
      configured.wep_key0 = net.wep_key0 = isWepHexKey(net.wep) ? net.wep : quote(net.wep);
      configured.auth_alg = net.auth_alg = "OPEN SHARED";
    }

    function hasValidProperty(name) {
      return ((name in net) && net[name] != null);
    }

    if (hasValidProperty("eap")) {
      if (hasValidProperty("pin")) {
        net.pin = quote(net.pin);
      }

      if (hasValidProperty("phase1"))
        net.phase1 = quote(net.phase1);

      if (hasValidProperty("phase2")) {
        if (net.phase2 === "TLS") {
          net.phase2 = quote("autheap=" + net.phase2);
        } else { // PAP, MSCHAPV2, etc.
          net.phase2 = quote("auth=" + net.phase2);
        }
      }

      if (hasValidProperty("serverCertificate"))
        net.ca_cert = quote("keystore://WIFI_SERVERCERT_" + net.serverCertificate);

      if (hasValidProperty("subjectMatch"))
        net.subject_match = quote(net.subjectMatch);

      if (hasValidProperty("userCertificate")) {
        let userCertName = "WIFI_USERCERT_" + net.userCertificate;
        net.client_cert = quote("keystore://" + userCertName);

        let wifiCertService = Cc["@mozilla.org/wifi/certservice;1"].
                                getService(Ci.nsIWifiCertService);
        if (wifiCertService.hasPrivateKey(userCertName)) {
          if (WifiManager.sdkVersion() >= 19) {
            // Use openssol engine instead of keystore protocol after Kitkat.
            net.engine = 1;
            net.engine_id = quote("keystore");
            net.key_id = quote("WIFI_USERKEY_" + net.userCertificate);
          } else {
            net.private_key = quote("keystore://WIFI_USERKEY_" + net.userCertificate);
          }
        }
      }
    }

    return net;
  };

  WifiManager.onsupplicantconnection = function() {
    debug("Connected to supplicant");
    // Give it a state other than UNINITIALIZED, INITIALIZING or DISABLING
    // defined in getter function of WifiManager.enabled. It implies that
    // we are ready to accept dom request from applications.
    WifiManager.state = "DISCONNECTED";
    self._reloadConfiguredNetworks(function(ok) {
      // Prime this.networks.
      if (!ok)
        return;

      // The select network command we used in associate() disables others networks.
      // Enable them here to make sure wpa_supplicant helps to connect to known
      // network automatically.
      self._enableAllNetworks();
      WifiManager.saveConfig(function() {})
    });

    // Notify everybody, even if they didn't ask us to come up.
    WifiManager.getMacAddress(function (mac) {
      self.macAddress = mac;
      debug("Got mac: " + mac);
      self._fireEvent("wifiUp", { macAddress: mac });
      self.requestDone();
    });

    if (WifiManager.state === "SCANNING")
      startScanStuckTimer();
  };

  WifiManager.onsupplicantlost = function() {
    WifiManager.supplicantStarted = false;
    WifiManager.state = "UNINITIALIZED";
    debug("Supplicant died!");

    // Notify everybody, even if they didn't ask us to come up.
    self._fireEvent("wifiDown", {});
    self.requestDone();
  };

  WifiManager.onpasswordmaybeincorrect = function() {
    WifiManager.authenticationFailuresCount++;
  };

  WifiManager.ondisconnected = function() {
    // We may fail to establish the connection, re-enable the
    // rest of our networks.
    if (self._needToEnableNetworks) {
      self._enableAllNetworks();
      self._needToEnableNetworks = false;
    }

    let connectionInfo = this.connectionInfo;
    WifiManager.getNetworkId(connectionInfo.ssid, function(netId) {
      // Trying to get netId from current network.
      if (!netId &&
          self.currentNetwork && self.currentNetwork.ssid &&
          dequote(self.currentNetwork.ssid) == connectionInfo.ssid &&
          typeof self.currentNetwork.netId !== "undefined") {
        netId = self.currentNetwork.netId;
      }
      if (netId) {
        WifiManager.disableNetwork(netId, function() {});
      }
    });
    self._fireEvent("onconnectingfailed", {network: netToDOM(self.currentNetwork)});
  }

  WifiManager.onstatechange = function() {
    debug("State change: " + this.prevState + " -> " + this.state);

    if (self._connectionInfoTimer &&
        this.state !== "CONNECTED" &&
        this.state !== "COMPLETED") {
      self._stopConnectionInfoTimer();
    }

    if (this.state !== "SCANNING" &&
        self._scanStuckTimer) {
      self._scanStuckTimer.cancel();
      self._scanStuckTimer = null;
    }

    switch (this.state) {
      case "DORMANT":
        // The dormant state is a bad state to be in since we won't
        // automatically connect. Try to knock us out of it. We only
        // hit this state when we've failed to run DHCP, so trying
        // again isn't the worst thing we can do. Eventually, we'll
        // need to detect if we're looping in this state and bail out.
        WifiManager.reconnect(function(){});
        break;
      case "ASSOCIATING":
        // id has not yet been filled in, so we can only report the ssid and
        // bssid. mode and frequency are simply made up.
        self.currentNetwork =
          { bssid: WifiManager.connectionInfo.bssid,
            ssid: quote(WifiManager.connectionInfo.ssid),
            mode: MODE_ESS,
            frequency: 0};
        WifiManager.getNetworkConfiguration(self.currentNetwork, function (){
          // Notify again because we get complete network information.
          self._fireEvent("onconnecting", { network: netToDOM(self.currentNetwork) });
        });
        break;
      case "ASSOCIATED":
        // set to full power mode when ready to do 4 way handsharke.
        WifiManager.setPowerSavingMode(false);
        if (!self.currentNetwork) {
          self.currentNetwork =
            { bssid: WifiManager.connectionInfo.bssid,
              ssid: quote(WifiManager.connectionInfo.ssid) };
        }
        self.currentNetwork.netId = this.id;
        WifiManager.getNetworkConfiguration(self.currentNetwork, function (){
          // Notify again because we get complete network information.
          self._fireEvent("onconnecting", { network: netToDOM(self.currentNetwork) });
        });
        break;
      case "COMPLETED":
        // Now that we've successfully completed the connection, re-enable the
        // rest of our networks.
        // XXX Need to do this eventually if the user entered an incorrect
        // password. For now, we require user interaction to break the loop and
        // select a better network!
        if (self._needToEnableNetworks) {
          self._enableAllNetworks();
          self._needToEnableNetworks = false;
        }

        var _oncompleted = function() {
          // The full authentication process is completed, reset the count.
          WifiManager.authenticationFailuresCount = 0;
          WifiManager.loopDetectionCount = 0;
          self._startConnectionInfoTimer();
          self._fireEvent("onassociate", { network: netToDOM(self.currentNetwork) });
        };

        // We get the ASSOCIATED event when we've associated but not connected, so
        // wait until the handshake is complete.
        if (this.fromStatus || !self.currentNetwork) {
          // In this case, we connected to an already-connected wpa_supplicant,
          // because of that we need to gather information about the current
          // network here.
          self.currentNetwork = { bssid: WifiManager.connectionInfo.bssid,
                                  ssid: quote(WifiManager.connectionInfo.ssid),
                                  netId: WifiManager.connectionInfo.id };
          WifiManager.getNetworkConfiguration(self.currentNetwork, _oncompleted);
        } else {
          _oncompleted();
        }
        break;
      case "CONNECTED":
        // wifi connection complete, turn on the power saving mode.
        WifiManager.setPowerSavingMode(true);
        // BSSID is read after connected, update it.
        self.currentNetwork.bssid = WifiManager.connectionInfo.bssid;
        break;
      case "DISCONNECTED":
        // wpa_supplicant may give us a "DISCONNECTED" event even if
        // we are already in "DISCONNECTED" state.
        if ((WifiNetworkInterface.info.state ===
             Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED) &&
             (this.prevState === "INITIALIZING" ||
              this.prevState === "DISCONNECTED" ||
              this.prevState === "INTERFACE_DISABLED" ||
              this.prevState === "INACTIVE" ||
              this.prevState === "UNINITIALIZED")) {
          // When in disconnected mode, need to turn on wifi power saving mode.
          WifiManager.setPowerSavingMode(true);
          return;
        }

        self._fireEvent("ondisconnect", {network: netToDOM(self.currentNetwork)});

        self.currentNetwork = null;
        self.ipAddress = "";

        if (self._turnOnBackgroundScan) {
          self._turnOnBackgroundScan = false;
          WifiManager.setBackgroundScan("ON", function(did_something, ok) {
            WifiManager.reassociate(function() {});
          });
        }

        WifiManager.connectionDropped(function() {
          // We've disconnected from a network because of a call to forgetNetwork.
          // Reconnect to the next available network (if any).
          if (self._reconnectOnDisconnect) {
            self._reconnectOnDisconnect = false;
            WifiManager.reconnect(function(){});
          }
        });

        WifiNetworkInterface.info.state =
          Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED;

        // Update network infterface first then clear properties.
        gNetworkManager.updateNetworkInterface(WifiNetworkInterface);
        WifiNetworkInterface.info.ips = [];
        WifiNetworkInterface.info.prefixLengths = [];
        WifiNetworkInterface.info.gateways = [];
        WifiNetworkInterface.info.dnses = [];


        break;
      case "WPS_TIMEOUT":
        self._fireEvent("onwpstimeout", {});
        break;
      case "WPS_FAIL":
        self._fireEvent("onwpsfail", {});
        break;
      case "WPS_OVERLAP_DETECTED":
        self._fireEvent("onwpsoverlap", {});
        break;
      case "AUTHENTICATING":
        self._fireEvent("onauthenticating", {network: netToDOM(self.currentNetwork)});
        break;
      case "SCANNING":
        // If we're already scanning in the background, we don't need to worry
        // about getting stuck while scanning.
        if (!WifiManager.backgroundScanEnabled && WifiManager.enabled)
          startScanStuckTimer();
        break;
    }
  };

  WifiManager.onnetworkconnected = function() {
    if (!this.info || !this.info.ipaddr_str) {
      debug("Network information is invalid.");
      return;
    }

    let maskLength =
      netHelpers.getMaskLength(netHelpers.stringToIP(this.info.mask_str));
    if (!maskLength) {
      maskLength = 32; // max prefix for IPv4.
    }

    let netConnect = WifiManager.getHttpProxyNetwork(self.currentNetwork);
    if (netConnect) {
      WifiNetworkInterface.httpProxyHost = netConnect.httpProxyHost;
      WifiNetworkInterface.httpProxyPort = netConnect.httpProxyPort;
    }

    WifiNetworkInterface.info.state =
      Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED;
    WifiNetworkInterface.info.ips = [this.info.ipaddr_str];
    WifiNetworkInterface.info.prefixLengths = [maskLength];
    WifiNetworkInterface.info.gateways = [this.info.gateway_str];
    if (typeof this.info.dns1_str == "string" &&
        this.info.dns1_str.length) {
      WifiNetworkInterface.info.dnses.push(this.info.dns1_str);
    }
    if (typeof this.info.dns2_str == "string" &&
        this.info.dns2_str.length) {
      WifiNetworkInterface.info.dnses.push(this.info.dns2_str);
    }
    gNetworkManager.updateNetworkInterface(WifiNetworkInterface);

    self.ipAddress = this.info.ipaddr_str;

    // We start the connection information timer when we associate, but
    // don't have our IP address until here. Make sure that we fire a new
    // connectionInformation event with the IP address the next time the
    // timer fires.
    self._lastConnectionInfo = null;
    self._fireEvent("onconnect", { network: netToDOM(self.currentNetwork) });
  };

  WifiManager.onscanresultsavailable = function() {
    if (self._scanStuckTimer) {
      // We got scan results! We must not be stuck for now, try again.
      self._scanStuckTimer.cancel();
      self._scanStuckTimer.initWithCallback(scanIsStuck, SCAN_STUCK_WAIT,
                                            Ci.nsITimer.TYPE_ONE_SHOT);
    }

    if (self.wantScanResults.length === 0) {
      debug("Scan results available, but we don't need them");
      return;
    }

    debug("Scan results are available! Asking for them.");
    WifiManager.getScanResults(function(r) {
      // Failure.
      if (!r) {
        self.wantScanResults.forEach(function(callback) { callback(null) });
        self.wantScanResults = [];
        return;
      }

      let capabilities = WifiManager.getCapabilities();

      // Now that we have scan results, there's no more need to continue
      // scanning. Ignore any errors from this command.
      WifiManager.setScanMode("inactive", function() {});
      let lines = r.split("\n");
      // NB: Skip the header line.
      self.networksArray = [];
      for (let i = 1; i < lines.length; ++i) {
        // bssid / frequency / signal level / flags / ssid
        var match = /([\S]+)\s+([\S]+)\s+([\S]+)\s+(\[[\S]+\])?\s(.*)/.exec(lines[i]);

        if (match && match[5]) {
          let ssid = match[5],
              bssid = match[1],
              frequency = match[2],
              signalLevel = match[3],
              flags = match[4];

          /* Skip networks with unknown or unsupported modes. */
          if (capabilities.mode.indexOf(getMode(flags)) == -1)
            continue;

          // If this is the first time that we've seen this SSID in the scan
          // results, add it to the list along with any other information.
          // Also, we use the highest signal strength that we see.
          let network = new ScanResult(ssid, bssid, frequency, flags, signalLevel);

          let networkKey = getNetworkKey(network);
          let eapIndex = -1;
          if (networkKey in self.configuredNetworks) {
            let known = self.configuredNetworks[networkKey];
            network.known = true;

            if ("identity" in known && known.identity)
              network.identity = dequote(known.identity);

            // Note: we don't hand out passwords here! The * marks that there
            // is a password that we're hiding.
            if (("psk" in known && known.psk) ||
                ("password" in known && known.password) ||
                ("wep_key0" in known && known.wep_key0)) {
              network.password = "*";
            }
          }

          self.networksArray.push(network);
          if (network.bssid === WifiManager.connectionInfo.bssid)
            network.connected = true;

          let signal = calculateSignal(Number(match[3]));
          if (signal > network.relSignalStrength)
            network.relSignalStrength = signal;
        } else if (!match) {
          debug("Match didn't find anything for: " + lines[i]);
        }
      }

      self.wantScanResults.forEach(function(callback) { callback(self.networksArray) });
      self.wantScanResults = [];
    });
  };

  WifiManager.onstationinfoupdate = function() {
    self._fireEvent("stationinfoupdate", { station: this.station });
  };

  WifiManager.onstopconnectioninfotimer = function() {
    self._stopConnectionInfoTimer();
  };

  // Read the 'wifi.enabled' setting in order to start with a known
  // value at boot time. The handle() will be called after reading.
  //
  // nsISettingsServiceCallback implementation.
  var initWifiEnabledCb = {
    handle: function handle(aName, aResult) {
      if (aName !== SETTINGS_WIFI_ENABLED)
        return;
      if (aResult === null)
        aResult = true;
      self.handleWifiEnabled(aResult);
    },
    handleError: function handleError(aErrorMessage) {
      debug("Error reading the 'wifi.enabled' setting. Default to wifi on.");
      self.handleWifiEnabled(true);
    }
  };

  var initWifiDebuggingEnabledCb = {
    handle: function handle(aName, aResult) {
      if (aName !== SETTINGS_WIFI_DEBUG_ENABLED)
        return;
      if (aResult === null)
        aResult = false;
      DEBUG = aResult;
      updateDebug();
    },
    handleError: function handleError(aErrorMessage) {
      debug("Error reading the 'wifi.debugging.enabled' setting. Default to debugging off.");
      DEBUG = false;
      updateDebug();
    }
  };

  this.initTetheringSettings();

  let lock = gSettingsService.createLock();
  lock.get(SETTINGS_WIFI_ENABLED, initWifiEnabledCb);
  lock.get(SETTINGS_WIFI_DEBUG_ENABLED, initWifiDebuggingEnabledCb);

  lock.get(SETTINGS_WIFI_SSID, this);
  lock.get(SETTINGS_WIFI_SECURITY_TYPE, this);
  lock.get(SETTINGS_WIFI_SECURITY_PASSWORD, this);
  lock.get(SETTINGS_WIFI_IP, this);
  lock.get(SETTINGS_WIFI_PREFIX, this);
  lock.get(SETTINGS_WIFI_DHCPSERVER_STARTIP, this);
  lock.get(SETTINGS_WIFI_DHCPSERVER_ENDIP, this);
  lock.get(SETTINGS_WIFI_DNS1, this);
  lock.get(SETTINGS_WIFI_DNS2, this);
  lock.get(SETTINGS_WIFI_TETHERING_ENABLED, this);

  lock.get(SETTINGS_USB_DHCPSERVER_STARTIP, this);
  lock.get(SETTINGS_USB_DHCPSERVER_ENDIP, this);

  this._wifiTetheringSettingsToRead = [SETTINGS_WIFI_SSID,
                                       SETTINGS_WIFI_SECURITY_TYPE,
                                       SETTINGS_WIFI_SECURITY_PASSWORD,
                                       SETTINGS_WIFI_IP,
                                       SETTINGS_WIFI_PREFIX,
                                       SETTINGS_WIFI_DHCPSERVER_STARTIP,
                                       SETTINGS_WIFI_DHCPSERVER_ENDIP,
                                       SETTINGS_WIFI_DNS1,
                                       SETTINGS_WIFI_DNS2,
                                       SETTINGS_WIFI_TETHERING_ENABLED,
                                       SETTINGS_USB_DHCPSERVER_STARTIP,
                                       SETTINGS_USB_DHCPSERVER_ENDIP];
}

function translateState(state) {
  switch (state) {
    case "INTERFACE_DISABLED":
    case "INACTIVE":
    case "SCANNING":
    case "DISCONNECTED":
    default:
      return "disconnected";

    case "AUTHENTICATING":
    case "ASSOCIATING":
    case "ASSOCIATED":
    case "FOUR_WAY_HANDSHAKE":
    case "GROUP_HANDSHAKE":
      return "connecting";

    case "COMPLETED":
      return WifiManager.getDhcpInfo() ? "connected" : "associated";
  }
}

WifiWorker.prototype = {
  classID:   WIFIWORKER_CID,
  classInfo: XPCOMUtils.generateCI({classID: WIFIWORKER_CID,
                                    contractID: WIFIWORKER_CONTRACTID,
                                    classDescription: "WifiWorker",
                                    interfaces: [Ci.nsIWorkerHolder,
                                                 Ci.nsIWifi,
                                                 Ci.nsIObserver]}),

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWorkerHolder,
                                         Ci.nsIWifi,
                                         Ci.nsIObserver,
                                         Ci.nsISettingsServiceCallback]),

  disconnectedByWifi: false,

  disconnectedByWifiTethering: false,

  _wifiTetheringSettingsToRead: [],

  _oldWifiTetheringEnabledState: null,

  tetheringSettings: {},

  initTetheringSettings: function initTetheringSettings() {
    this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] = null;
    this.tetheringSettings[SETTINGS_WIFI_SSID] = DEFAULT_WIFI_SSID;
    this.tetheringSettings[SETTINGS_WIFI_SECURITY_TYPE] = DEFAULT_WIFI_SECURITY_TYPE;
    this.tetheringSettings[SETTINGS_WIFI_SECURITY_PASSWORD] = DEFAULT_WIFI_SECURITY_PASSWORD;
    this.tetheringSettings[SETTINGS_WIFI_IP] = DEFAULT_WIFI_IP;
    this.tetheringSettings[SETTINGS_WIFI_PREFIX] = DEFAULT_WIFI_PREFIX;
    this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_STARTIP] = DEFAULT_WIFI_DHCPSERVER_STARTIP;
    this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_ENDIP] = DEFAULT_WIFI_DHCPSERVER_ENDIP;
    this.tetheringSettings[SETTINGS_WIFI_DNS1] = DEFAULT_DNS1;
    this.tetheringSettings[SETTINGS_WIFI_DNS2] = DEFAULT_DNS2;

    this.tetheringSettings[SETTINGS_USB_DHCPSERVER_STARTIP] = DEFAULT_USB_DHCPSERVER_STARTIP;
    this.tetheringSettings[SETTINGS_USB_DHCPSERVER_ENDIP] = DEFAULT_USB_DHCPSERVER_ENDIP;
  },

  // Internal methods.
  waitForScan: function(callback) {
    this.wantScanResults.push(callback);
  },

  // In order to select a specific network, we disable the rest of the
  // networks known to us. However, in general, we want the supplicant to
  // connect to which ever network it thinks is best, so when we select the
  // proper network (or fail to), we need to re-enable the rest.
  _enableAllNetworks: function() {
    for (let key in this.configuredNetworks) {
      let net = this.configuredNetworks[key];
      WifiManager.enableNetwork(net.netId, false, function(ok) {
        net.disabled = ok ? 1 : 0;
      });
    }
  },

  _startConnectionInfoTimer: function() {
    if (this._connectionInfoTimer)
      return;

    var self = this;
    function getConnectionInformation() {
      WifiManager.getConnectionInfo(function(connInfo) {
        // See comments in calculateSignal for information about this.
        if (!connInfo) {
          self._lastConnectionInfo = null;
          return;
        }

        let { rssi, linkspeed } = connInfo;
        if (rssi > 0)
          rssi -= 256;
        if (rssi <= MIN_RSSI)
          rssi = MIN_RSSI;
        else if (rssi >= MAX_RSSI)
          rssi = MAX_RSSI;

        let info = { signalStrength: rssi,
                     relSignalStrength: calculateSignal(rssi),
                     linkSpeed: linkspeed,
                     ipAddress: self.ipAddress };
        let last = self._lastConnectionInfo;

        // Only fire the event if the link speed changed or the signal
        // strength changed by more than 10%.
        function tensPlace(percent) {
          return (percent / 10) | 0;
        }

        if (last && last.linkSpeed === info.linkSpeed &&
            last.ipAddress === info.ipAddress &&
            tensPlace(last.relSignalStrength) === tensPlace(info.relSignalStrength)) {
          return;
        }

        self._lastConnectionInfo = info;
        debug("Firing connectioninfoupdate: " + uneval(info));
        self._fireEvent("connectioninfoupdate", info);
      });
    }

    // Prime our _lastConnectionInfo immediately and fire the event at the
    // same time.
    getConnectionInformation();

    // Now, set up the timer for regular updates.
    this._connectionInfoTimer =
      Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._connectionInfoTimer.init(getConnectionInformation, 5000,
                                   Ci.nsITimer.TYPE_REPEATING_SLACK);
  },

  _stopConnectionInfoTimer: function() {
    if (!this._connectionInfoTimer)
      return;

    this._connectionInfoTimer.cancel();
    this._connectionInfoTimer = null;
    this._lastConnectionInfo = null;
  },

  _reloadConfiguredNetworks: function(callback) {
    WifiManager.getConfiguredNetworks((function(networks) {
      if (!networks) {
        debug("Unable to get configured networks");
        callback(false);
        return;
      }

      this._highestPriority = -1;

      // Convert between netId-based and ssid-based indexing.
      for (let net in networks) {
        let network = networks[net];
        delete networks[net];

        if (!network.ssid) {
          WifiManager.removeNetwork(network.netId, function() {});
          continue;
        }

        if (network.hasOwnProperty("priority") &&
            network.priority > this._highestPriority) {
          this._highestPriority = network.priority;
        }

        let networkKey = getNetworkKey(network);
        // Accept latest config of same network(same SSID and same security).
        if (networks[networkKey]) {
          WifiManager.removeNetwork(networks[networkKey].netId, function() {});
        }
        networks[networkKey] = network;
      }

      this.configuredNetworks = networks;
      callback(true);
    }).bind(this));
  },

  // Important side effect: calls WifiManager.saveConfig.
  _reprioritizeNetworks: function(callback) {
    // First, sort the networks in orer of their priority.
    var ordered = Object.getOwnPropertyNames(this.configuredNetworks);
    let self = this;
    ordered.sort(function(a, b) {
      var neta = self.configuredNetworks[a],
          netb = self.configuredNetworks[b];

      // Sort unsorted networks to the end of the list.
      if (isNaN(neta.priority))
        return isNaN(netb.priority) ? 0 : 1;
      if (isNaN(netb.priority))
        return -1;
      return netb.priority - neta.priority;
    });

    // Skip unsorted networks.
    let newPriority = 0, i;
    for (i = ordered.length - 1; i >= 0; --i) {
      if (!isNaN(this.configuredNetworks[ordered[i]].priority))
        break;
    }

    // No networks we care about?
    if (i < 0) {
      WifiManager.saveConfig(callback);
      return;
    }

    // Now assign priorities from 0 to length, starting with the smallest
    // priority and heading towards the highest (note the dependency between
    // total and i here).
    let done = 0, errors = 0, total = i + 1;
    for (; i >= 0; --i) {
      let network = this.configuredNetworks[ordered[i]];
      network.priority = newPriority++;

      // Note: networkUpdated declared below since it happens logically after
      // this loop.
      WifiManager.updateNetwork(network, networkUpdated);
    }

    function networkUpdated(ok) {
      if (!ok)
        ++errors;
      if (++done === total) {
        if (errors > 0) {
          callback(false);
          return;
        }

        WifiManager.saveConfig(function(ok) {
          if (!ok) {
            callback(false);
            return;
          }

          self._reloadConfiguredNetworks(function(ok) {
            callback(ok);
          });
        });
      }
    }
  },

  // nsIWifi

  _domManagers: [],
  _fireEvent: function(message, data) {
    this._domManagers.forEach(function(manager) {
      // Note: We should never have a dead message manager here because we
      // observe our child message managers shutting down, below.
      manager.sendAsyncMessage("WifiManager:" + message, data);
    });
  },

  _sendMessage: function(message, success, data, msg) {
    try {
      msg.manager.sendAsyncMessage(message + (success ? ":OK" : ":NO"),
                                   { data: data, rid: msg.rid, mid: msg.mid });
    } catch (e) {
      debug("sendAsyncMessage error : " + e);
    }
    this._splicePendingRequest(msg);
  },

  _domRequest: [],

  _splicePendingRequest: function(msg) {
    for (let i = 0; i < this._domRequest.length; i++) {
      if (this._domRequest[i].msg === msg) {
        this._domRequest.splice(i, 1);
        return;
      }
    }
  },

  _clearPendingRequest: function() {
    if (this._domRequest.length === 0) return;
    this._domRequest.forEach((function(req) {
      this._sendMessage(req.name + ":Return", false, "Wifi is disabled", req.msg);
    }).bind(this));
  },

  receiveMessage: function MessageManager_receiveMessage(aMessage) {
    let msg = aMessage.data || {};
    msg.manager = aMessage.target;

    if (WifiManager.p2pSupported()) {
      // If p2pObserver returns something truthy, return it!
      // Otherwise, continue to do the rest of tasks.
      var p2pRet = this._p2pObserver.onDOMMessage(aMessage);
      if (p2pRet) {
        return p2pRet;
      }
    }

    // Note: By the time we receive child-process-shutdown, the child process
    // has already forgotten its permissions so we do this before the
    // permissions check.
    if (aMessage.name === "child-process-shutdown") {
      let i;
      if ((i = this._domManagers.indexOf(msg.manager)) != -1) {
        this._domManagers.splice(i, 1);
      }
      for (i = this._domRequest.length - 1; i >= 0; i--) {
        if (this._domRequest[i].msg.manager === msg.manager) {
          this._domRequest.splice(i, 1);
        }
      }
      return;
    }

    if (!aMessage.target.assertPermission("wifi-manage")) {
      return;
    }

    // We are interested in DOMRequests only.
    if (aMessage.name != "WifiManager:getState") {
      this._domRequest.push({name: aMessage.name, msg:msg});
    }

    switch (aMessage.name) {
      case "WifiManager:setWifiEnabled":
        this.setWifiEnabled(msg);
        break;
      case "WifiManager:getNetworks":
        this.getNetworks(msg);
        break;
      case "WifiManager:getKnownNetworks":
        this.getKnownNetworks(msg);
        break;
      case "WifiManager:associate":
        this.associate(msg);
        break;
      case "WifiManager:forget":
        this.forget(msg);
        break;
      case "WifiManager:wps":
        this.wps(msg);
        break;
      case "WifiManager:setPowerSavingMode":
        this.setPowerSavingMode(msg);
        break;
      case "WifiManager:setHttpProxy":
        this.setHttpProxy(msg);
        break;
      case "WifiManager:setStaticIpMode":
        this.setStaticIpMode(msg);
        break;
      case "WifiManager:importCert":
        this.importCert(msg);
        break;
      case "WifiManager:getImportedCerts":
        this.getImportedCerts(msg);
        break;
      case "WifiManager:deleteCert":
        this.deleteCert(msg);
        break;
      case "WifiManager:setWifiTethering":
        this.setWifiTethering(msg);
        break;
      case "WifiManager:getState": {
        let i;
        if ((i = this._domManagers.indexOf(msg.manager)) === -1) {
          this._domManagers.push(msg.manager);
        }

        let net = this.currentNetwork ? netToDOM(this.currentNetwork) : null;
        return { network: net,
                 connectionInfo: this._lastConnectionInfo,
                 enabled: WifiManager.enabled,
                 status: translateState(WifiManager.state),
                 macAddress: this.macAddress,
                 capabilities: WifiManager.getCapabilities()};
      }
    }
  },

  getNetworks: function(msg) {
    const message = "WifiManager:getNetworks:Return";
    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    let sent = false;
    let callback = (function (networks) {
      if (sent)
        return;
      sent = true;
      this._sendMessage(message, networks !== null, networks, msg);
    }).bind(this);
    this.waitForScan(callback);

    WifiManager.scan(true, (function(ok) {
      // If the scan command succeeded, we're done.
      if (ok)
        return;

      // Avoid sending multiple responses.
      if (sent)
        return;

      // Otherwise, let the client know that it failed, it's responsible for
      // trying again in a few seconds.
      sent = true;
      this._sendMessage(message, false, "ScanFailed", msg);
    }).bind(this));
  },

  getWifiScanResults: function(callback) {
    var count = 0;
    var timer = null;
    var self = this;

    if (!WifiManager.enabled) {
      callback.onfailure();
      return;
    }

    self.waitForScan(waitForScanCallback);
    doScan();
    function doScan() {
      WifiManager.scan(true, (function (ok) {
        if (!ok) {
          if (!timer) {
            count = 0;
            timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          }

          if (count++ >= 3) {
            timer = null;
            self.wantScanResults.splice(self.wantScanResults.indexOf(waitForScanCallback), 1);
            callback.onfailure();
            return;
          }

          // Else it's still running, continue waiting.
          timer.initWithCallback(doScan, 10000, Ci.nsITimer.TYPE_ONE_SHOT);
          return;
        }
      }).bind(this));
    }

    function waitForScanCallback(networks) {
      if (networks === null) {
        callback.onfailure();
        return;
      }

      var wifiScanResults = new Array();
      var net;
      for (let net in networks) {
        let value = networks[net];
        wifiScanResults.push(transformResult(value));
      }
      callback.onready(wifiScanResults.length, wifiScanResults);
    }

    function transformResult(element) {
      var result = new WifiScanResult();
      result.connected = false;
      for (let id in element) {
        if (id === "__exposedProps__") {
          continue;
        }
        if (id === "security") {
          result[id] = 0;
          var security = element[id];
          for (let j = 0; j < security.length; j++) {
            if (security[j] === "WPA-PSK") {
              result[id] |= Ci.nsIWifiScanResult.WPA_PSK;
            } else if (security[j] === "WPA-EAP") {
              result[id] |= Ci.nsIWifiScanResult.WPA_EAP;
            } else if (security[j] === "WEP") {
              result[id] |= Ci.nsIWifiScanResult.WEP;
            } else {
             result[id] = 0;
            }
          }
        } else {
          result[id] = element[id];
        }
      }
      return result;
    }
  },

  getKnownNetworks: function(msg) {
    const message = "WifiManager:getKnownNetworks:Return";
    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    this._reloadConfiguredNetworks((function(ok) {
      if (!ok) {
        this._sendMessage(message, false, "Failed", msg);
        return;
      }

      var networks = [];
      for (let networkKey in this.configuredNetworks) {
        networks.push(netToDOM(this.configuredNetworks[networkKey]));
      }

      this._sendMessage(message, true, networks, msg);
    }).bind(this));
  },

  _setWifiEnabledCallback: function(status) {
    if (status !== 0) {
      this.requestDone();
      return;
    }

    // If we're enabling ourselves, then wait until we've connected to the
    // supplicant to notify. If we're disabling, we take care of this in
    // supplicantlost.
    if (WifiManager.supplicantStarted)
      WifiManager.start();
  },

  /**
   * Compatibility flags for detecting if Gaia is controlling wifi by settings
   * or API, once API is called, gecko will no longer accept wifi enable
   * control from settings.
   * This is used to deal with compatibility issue while Gaia adopted to use
   * API but gecko doesn't remove the settings code in time.
   * TODO: Remove this flag in Bug 1050147
   */
  ignoreWifiEnabledFromSettings: false,
  setWifiEnabled: function(msg) {
    const message = "WifiManager:setWifiEnabled:Return";
    let self = this;
    let enabled = msg.data;

    self.ignoreWifiEnabledFromSettings = true;
    // No change.
    if (enabled === WifiManager.enabled) {
      this._sendMessage(message, true, true, msg);
      return;
    }

    // Can't enable wifi while hotspot mode is enabled.
    if (enabled && (this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] ||
        WifiManager.isWifiTetheringEnabled(WifiManager.tetheringState))) {
      self._sendMessage(message, false, "Can't enable Wifi while hotspot mode is enabled", msg);
      return;
    }

    WifiManager.setWifiEnabled(enabled, function(ok) {
      if (ok === 0 || ok === "no change") {
        self._sendMessage(message, true, true, msg);

        // Reply error to pending requests.
        if (!enabled) {
          self._clearPendingRequest();
        } else {
          WifiManager.start();
        }
      } else {
        self._sendMessage(message, false, "Set wifi enabled failed", msg);
      }
    });
  },

  _setWifiEnabled: function(enabled, callback) {
    // Reply error to pending requests.
    if (!enabled) {
      this._clearPendingRequest();
    }

    WifiManager.setWifiEnabled(enabled, callback);
  },

  // requestDone() must be called to before callback complete(or error)
  // so next queue in the request quene can be executed.
  // TODO: Remove command queue in Bug 1050147
  queueRequest: function(data, callback) {
    if (!callback) {
        throw "Try to enqueue a request without callback";
    }

    let optimizeCommandList = ["setWifiEnabled", "setWifiApEnabled"];
    if (optimizeCommandList.indexOf(data.command) != -1) {
      this._stateRequests = this._stateRequests.filter(function(element) {
        return element.data.command !== data.command;
      });
    }

    this._stateRequests.push({
      data: data,
      callback: callback
    });

    this.nextRequest();
  },

  getWifiTetheringParameters: function getWifiTetheringParameters(enable) {
    if (this.useTetheringAPI) {
      return this.getWifiTetheringConfiguration(enable);
    } else {
      return this.getWifiTetheringParametersBySetting(enable);
    }
  },

  getWifiTetheringConfiguration: function getWifiTetheringConfiguration(enable) {
    let config = {};
    let params = this.tetheringConfig;

    let check = function(field, _default) {
      config[field] = field in params ? params[field] : _default;
    };

    check("ssid", DEFAULT_WIFI_SSID);
    check("security", DEFAULT_WIFI_SECURITY_TYPE);
    check("key", DEFAULT_WIFI_SECURITY_PASSWORD);
    check("ip", DEFAULT_WIFI_IP);
    check("prefix", DEFAULT_WIFI_PREFIX);
    check("wifiStartIp", DEFAULT_WIFI_DHCPSERVER_STARTIP);
    check("wifiEndIp", DEFAULT_WIFI_DHCPSERVER_ENDIP);
    check("usbStartIp", DEFAULT_USB_DHCPSERVER_STARTIP);
    check("usbEndIp", DEFAULT_USB_DHCPSERVER_ENDIP);
    check("dns1", DEFAULT_DNS1);
    check("dns2", DEFAULT_DNS2);

    config.enable = enable;
    config.mode = enable ? WIFI_FIRMWARE_AP : WIFI_FIRMWARE_STATION;
    config.link = enable ? NETWORK_INTERFACE_UP : NETWORK_INTERFACE_DOWN;

    // Check the format to prevent netd from crash.
    if (enable && (!config.ssid || config.ssid == "")) {
      debug("Invalid SSID value.");
      return null;
    }

    if (enable && (config.security != WIFI_SECURITY_TYPE_NONE && !config.key)) {
      debug("Invalid security password.");
      return null;
    }

    // Using the default values here until application supports these settings.
    if (config.ip == "" || config.prefix == "" ||
        config.wifiStartIp == "" || config.wifiEndIp == "" ||
        config.usbStartIp == "" || config.usbEndIp == "") {
      debug("Invalid subnet information.");
      return null;
    }

    return config;
  },

  getWifiTetheringParametersBySetting: function getWifiTetheringParametersBySetting(enable) {
    let ssid;
    let securityType;
    let securityId;
    let interfaceIp;
    let prefix;
    let wifiDhcpStartIp;
    let wifiDhcpEndIp;
    let usbDhcpStartIp;
    let usbDhcpEndIp;
    let dns1;
    let dns2;

    ssid = this.tetheringSettings[SETTINGS_WIFI_SSID];
    securityType = this.tetheringSettings[SETTINGS_WIFI_SECURITY_TYPE];
    securityId = this.tetheringSettings[SETTINGS_WIFI_SECURITY_PASSWORD];
    interfaceIp = this.tetheringSettings[SETTINGS_WIFI_IP];
    prefix = this.tetheringSettings[SETTINGS_WIFI_PREFIX];
    wifiDhcpStartIp = this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_STARTIP];
    wifiDhcpEndIp = this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_ENDIP];
    usbDhcpStartIp = this.tetheringSettings[SETTINGS_USB_DHCPSERVER_STARTIP];
    usbDhcpEndIp = this.tetheringSettings[SETTINGS_USB_DHCPSERVER_ENDIP];
    dns1 = this.tetheringSettings[SETTINGS_WIFI_DNS1];
    dns2 = this.tetheringSettings[SETTINGS_WIFI_DNS2];

    // Check the format to prevent netd from crash.
    if (!ssid || ssid == "") {
      debug("Invalid SSID value.");
      return null;
    }
    // Truncate ssid if its length of encoded to utf8 is longer than 32.
    while (unescape(encodeURIComponent(ssid)).length > 32)
    {
      ssid = ssid.substring(0, ssid.length-1);
    }

    if (securityType != WIFI_SECURITY_TYPE_NONE &&
        securityType != WIFI_SECURITY_TYPE_WPA_PSK &&
        securityType != WIFI_SECURITY_TYPE_WPA2_PSK) {

      debug("Invalid security type.");
      return null;
    }
    if (securityType != WIFI_SECURITY_TYPE_NONE && !securityId) {
      debug("Invalid security password.");
      return null;
    }
    // Using the default values here until application supports these settings.
    if (interfaceIp == "" || prefix == "" ||
        wifiDhcpStartIp == "" || wifiDhcpEndIp == "" ||
        usbDhcpStartIp == "" || usbDhcpEndIp == "") {
      debug("Invalid subnet information.");
      return null;
    }

    return {
      ssid: ssid,
      security: securityType,
      key: securityId,
      ip: interfaceIp,
      prefix: prefix,
      wifiStartIp: wifiDhcpStartIp,
      wifiEndIp: wifiDhcpEndIp,
      usbStartIp: usbDhcpStartIp,
      usbEndIp: usbDhcpEndIp,
      dns1: dns1,
      dns2: dns2,
      enable: enable,
      mode: enable ? WIFI_FIRMWARE_AP : WIFI_FIRMWARE_STATION,
      link: enable ? NETWORK_INTERFACE_UP : NETWORK_INTERFACE_DOWN
    };
  },

  setWifiApEnabled: function(enabled, callback) {
    let configuration = this.getWifiTetheringParameters(enabled);

    if (!configuration) {
      this.requestDone();
      debug("Invalid Wifi Tethering configuration.");
      return;
    }

    WifiManager.setWifiApEnabled(enabled, configuration, callback);
  },

  associate: function(msg) {
    const MAX_PRIORITY = 9999;
    const message = "WifiManager:associate:Return";
    let network = msg.data;

    let privnet = network;
    let dontConnect = privnet.dontConnect;
    delete privnet.dontConnect;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    let self = this;
    function networkReady() {
      // saveConfig now before we disable most of the other networks.
      function selectAndConnect() {
        WifiManager.enableNetwork(privnet.netId, true, function (ok) {
          if (ok)
            self._needToEnableNetworks = true;
          if (WifiManager.state === "DISCONNECTED" ||
              WifiManager.state === "SCANNING") {
            WifiManager.reconnect(function (ok) {
              self._sendMessage(message, ok, ok, msg);
            });
          } else {
            self._sendMessage(message, ok, ok, msg);
          }
        });
      }

      var selectAndConnectOrReturn = dontConnect ?
        function() {
          self._sendMessage(message, true, "Wifi has been recorded", msg);
        } : selectAndConnect;
      if (self._highestPriority >= MAX_PRIORITY) {
        self._reprioritizeNetworks(selectAndConnectOrReturn);
      } else {
        WifiManager.saveConfig(selectAndConnectOrReturn);
      }
    }

    function connectToNetwork() {
      WifiManager.updateNetwork(privnet, (function(ok) {
        if (!ok) {
          self._sendMessage(message, false, "Network is misconfigured", msg);
          return;
        }

        networkReady();
      }));
    }

    let ssid = privnet.ssid;
    let networkKey = getNetworkKey(privnet);
    let configured;

    if (networkKey in this._addingNetworks) {
      this._sendMessage(message, false, "Racing associates");
      return;
    }

    if (networkKey in this.configuredNetworks)
      configured = this.configuredNetworks[networkKey];

    netFromDOM(privnet, configured);

    privnet.priority = ++this._highestPriority;
    if (configured) {
      privnet.netId = configured.netId;
      // Sync priority back to configured so if priority reaches MAX_PRIORITY,
      // it can be sorted correctly in _reprioritizeNetworks() because the
      // function sort network based on priority in configure list.
      configured.priority = privnet.priority;

      // When investigating Bug 1123680, we observed that gaia may unexpectedly
      // request to associate with incorrect password before successfully
      // forgetting the network. It would cause the network unable to connect
      // subsequently. Aside from preventing the racing forget/associate, we
      // also try to disable network prior to updating the network.
      WifiManager.getNetworkId(dequote(configured.ssid), function(netId) {
        if (netId) {
          WifiManager.disableNetwork(netId, function() {
            connectToNetwork();
          });
        }
        else {
          connectToNetwork();
        }
      });
    } else {
      // networkReady, above, calls saveConfig. We want to remember the new
      // network as being enabled, which isn't the default, so we explicitly
      // set it to being "enabled" before we add it and save the
      // configuration.
      privnet.disabled = 0;
      this._addingNetworks[networkKey] = privnet;
      WifiManager.addNetwork(privnet, (function(ok) {
        delete this._addingNetworks[networkKey];

        if (!ok) {
          this._sendMessage(message, false, "Network is misconfigured", msg);
          return;
        }

        this.configuredNetworks[networkKey] = privnet;
        networkReady();
      }).bind(this));
    }
  },

  forget: function(msg) {
    const message = "WifiManager:forget:Return";
    let network = msg.data;
    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    this._reloadConfiguredNetworks((function(ok) {
      // Give it a chance to remove the network even if reload is failed.
      if (!ok) {
        debug("Warning !!! Failed to reload the configured networks");
      }

      let ssid = network.ssid;
      let networkKey = getNetworkKey(network);
      if (!(networkKey in this.configuredNetworks)) {
        this._sendMessage(message, false, "Trying to forget an unknown network", msg);
        return;
      }

      let self = this;
      let configured = this.configuredNetworks[networkKey];
      this._reconnectOnDisconnect = (this.currentNetwork &&
                                    (this.currentNetwork.ssid === ssid));
      WifiManager.removeNetwork(configured.netId, function(ok) {
        if (self._needToEnableNetworks) {
          self._enableAllNetworks();
          self._needToEnableNetworks = false;
        }

        if (!ok) {
          self._sendMessage(message, false, "Unable to remove the network", msg);
          self._reconnectOnDisconnect = false;
          return;
        }

        WifiManager.saveConfig(function() {
          self._reloadConfiguredNetworks(function() {
            self._sendMessage(message, true, true, msg);
          });
        });
      });
    }).bind(this));
  },

  wps: function(msg) {
    const message = "WifiManager:wps:Return";
    let self = this;
    let detail = msg.data;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    if (detail.method === "pbc") {
      WifiManager.wpsPbc(function(ok) {
        if (ok)
          self._sendMessage(message, true, true, msg);
        else
          self._sendMessage(message, false, "WPS PBC failed", msg);
      });
    } else if (detail.method === "pin") {
      WifiManager.wpsPin(detail, function(pin) {
        if (pin)
          self._sendMessage(message, true, pin, msg);
        else
          self._sendMessage(message, false, "WPS PIN failed", msg);
      });
    } else if (detail.method === "cancel") {
      WifiManager.wpsCancel(function(ok) {
        if (ok)
          self._sendMessage(message, true, true, msg);
        else
          self._sendMessage(message, false, "WPS Cancel failed", msg);
      });
    } else {
      self._sendMessage(message, false, "Invalid WPS method=" + detail.method,
                        msg);
    }
  },

  setPowerSavingMode: function(msg) {
    const message = "WifiManager:setPowerSavingMode:Return";
    let self = this;
    let enabled = msg.data;
    let mode = enabled ? "AUTO" : "ACTIVE";

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    // Some wifi drivers may not implement this command. Set power mode
    // even if suspend optimization command failed.
    WifiManager.setSuspendOptimizations(enabled, function(ok) {
      WifiManager.setPowerMode(mode, function(ok) {
        if (ok) {
          self._sendMessage(message, true, true, msg);
        } else {
          self._sendMessage(message, false, "Set power saving mode failed", msg);
        }
      });
    });
  },

  setHttpProxy: function(msg) {
    const message = "WifiManager:setHttpProxy:Return";
    let self = this;
    let network = msg.data.network;
    let info = msg.data.info;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    WifiManager.configureHttpProxy(network, info, function(ok) {
      if (ok) {
        // If configured network is current connected network
        // need update http proxy immediately.
        let setNetworkKey = getNetworkKey(network);
        let curNetworkKey = self.currentNetwork ? getNetworkKey(self.currentNetwork) : null;
        if (setNetworkKey === curNetworkKey)
          WifiManager.setHttpProxy(network);

        self._sendMessage(message, true, true, msg);
      } else {
        self._sendMessage(message, false, "Set http proxy failed", msg);
      }
    });
  },

  setStaticIpMode: function(msg) {
    const message = "WifiManager:setStaticIpMode:Return";
    let self = this;
    let network = msg.data.network;
    let info = msg.data.info;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    // To compatiable with DHCP returned info structure, do translation here
    info.ipaddr_str = info.ipaddr;
    info.proxy_str = info.proxy;
    info.gateway_str = info.gateway;
    info.dns1_str = info.dns1;
    info.dns2_str = info.dns2;

    WifiManager.setStaticIpMode(network, info, function(ok) {
      if (ok) {
        self._sendMessage(message, true, true, msg);
      } else {
        self._sendMessage(message, false, "Set static ip mode failed", msg);
      }
    });
  },

  importCert: function importCert(msg) {
    const message = "WifiManager:importCert:Return";
    let self = this;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    WifiManager.importCert(msg.data, function(data) {
      if (data.status === 0) {
        let usageString = ["ServerCert", "UserCert"];
        let usageArray = [];
        for (let i = 0; i < usageString.length; i++) {
          if (data.usageFlag & (0x01 << i)) {
            usageArray.push(usageString[i]);
          }
        }

        self._sendMessage(message, true, {
          nickname: data.nickname,
          usage: usageArray
        }, msg);
      } else {
        self._sendMessage(message, false, "Import Cert failed", msg);
      }
    });
  },

  getImportedCerts: function getImportedCerts(msg) {
    const message = "WifiManager:getImportedCerts:Return";
    let self = this;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    let certDB = Cc["@mozilla.org/security/x509certdb;1"]
                 .getService(Ci.nsIX509CertDB);
    if (!certDB) {
      self._sendMessage(message, false, "Failed to query NSS DB service", msg);
    }

    let certList = certDB.getCerts();
    if (!certList) {
      self._sendMessage(message, false, "Failed to get certificate List", msg);
    }

    let certListEnum = certList.getEnumerator();
    if (!certListEnum) {
      self._sendMessage(message, false, "Failed to get certificate List enumerator", msg);
    }
    let importedCerts = {
      ServerCert: [],
      UserCert: [],
    };
    let UsageMapping = {
      SERVERCERT: "ServerCert",
      USERCERT: "UserCert",
    };

    while (certListEnum.hasMoreElements()) {
      let certInfo = certListEnum.getNext().QueryInterface(Ci.nsIX509Cert);
      let certNicknameInfo = /WIFI\_([A-Z]*)\_(.*)/.exec(certInfo.nickname);
      if (!certNicknameInfo) {
        continue;
      }
      importedCerts[UsageMapping[certNicknameInfo[1]]].push(certNicknameInfo[2]);
    }

    self._sendMessage(message, true, importedCerts, msg);
  },

  deleteCert: function deleteCert(msg) {
    const message = "WifiManager:deleteCert:Return";
    let self = this;

    if (!WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is disabled", msg);
      return;
    }

    WifiManager.deleteCert(msg.data, function(data) {
      self._sendMessage(message, data.status === 0, "Delete Cert failed", msg);
    });
  },

  // TODO : These two variables should be removed once GAIA uses tethering API.
  useTetheringAPI : false,
  tetheringConfig : {},

  setWifiTethering: function setWifiTethering(msg) {
    const message = "WifiManager:setWifiTethering:Return";
    let self = this;
    let enabled = msg.data.enabled;

    this.useTetheringAPI = true;
    this.tetheringConfig = msg.data.config;

    if (WifiManager.enabled) {
      this._sendMessage(message, false, "Wifi is enabled", msg);
      return;
    }

    this.setWifiApEnabled(enabled, function() {
      if ((enabled && WifiManager.tetheringState == "COMPLETED") ||
          (!enabled && WifiManager.tetheringState == "UNINITIALIZED")) {
        self._sendMessage(message, true, msg.data, msg);
      } else {
        msg.data.reason = enabled ?
          "Enable WIFI tethering faild" : "Disable WIFI tethering faild";
        self._sendMessage(message, false, msg.data, msg);
      }
    });
  },

  // This is a bit ugly, but works. In particular, this depends on the fact
  // that RadioManager never actually tries to get the worker from us.
  get worker() { throw "Not implemented"; },

  shutdown: function() {
    debug("shutting down ...");
    this.queueRequest({command: "setWifiEnabled", value: false}, function(data) {
      this._setWifiEnabled(false, this._setWifiEnabledCallback.bind(this));
    }.bind(this));
  },

  // TODO: Remove command queue in Bug 1050147.
  requestProcessing: false,   // Hold while dequeue and execution a request.
                              // Released upon the request is fully executed,
                              // i.e, mostly after callback is done.
  requestDone: function requestDone() {
    this.requestProcessing = false;
    this.nextRequest();
  },

  nextRequest: function nextRequest() {
    // No request to process
    if (this._stateRequests.length === 0) {
      return;
    }

    // Handling request, wait for it.
    if (this.requestProcessing) {
      return;
    }

    // Hold processing lock
    this.requestProcessing = true;

    // Find next valid request
    let request = this._stateRequests.shift();

    request.callback(request.data);
  },

  notifyTetheringOn: function notifyTetheringOn() {
    // It's really sad that we don't have an API to notify the wifi
    // hotspot status. Toggle settings to let gaia know that wifi hotspot
    // is enabled.
    let self = this;
    this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] = true;
    this._oldWifiTetheringEnabledState = true;
    gSettingsService.createLock().set(
      SETTINGS_WIFI_TETHERING_ENABLED,
      true,
      {
        handle: function(aName, aResult) {
          self.requestDone();
        },
        handleError: function(aErrorMessage) {
          self.requestDone();
        }
      });
  },

  notifyTetheringOff: function notifyTetheringOff() {
    // It's really sad that we don't have an API to notify the wifi
    // hotspot status. Toggle settings to let gaia know that wifi hotspot
    // is disabled.
    let self = this;
    this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] = false;
    this._oldWifiTetheringEnabledState = false;
    gSettingsService.createLock().set(
      SETTINGS_WIFI_TETHERING_ENABLED,
      false,
      {
        handle: function(aName, aResult) {
          self.requestDone();
        },
        handleError: function(aErrorMessage) {
          self.requestDone();
        }
      });
  },

  handleWifiEnabled: function(enabled) {
    if (this.ignoreWifiEnabledFromSettings) {
      return;
    }

    // Make sure Wifi hotspot is idle before switching to Wifi mode.
    if (enabled) {
      this.queueRequest({command: "setWifiApEnabled", value: false}, function(data) {
        if (this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] ||
            WifiManager.isWifiTetheringEnabled(WifiManager.tetheringState)) {
          this.disconnectedByWifi = true;
          this.setWifiApEnabled(false, this.notifyTetheringOff.bind(this));
        } else {
          this.requestDone();
        }
      }.bind(this));
    }

    this.queueRequest({command: "setWifiEnabled", value: enabled}, function(data) {
      this._setWifiEnabled(enabled, this._setWifiEnabledCallback.bind(this));
    }.bind(this));

    if (!enabled) {
      this.queueRequest({command: "setWifiApEnabled", value: true}, function(data) {
        if (this.disconnectedByWifi) {
          this.setWifiApEnabled(true, this.notifyTetheringOn.bind(this));
        } else {
          this.requestDone();
        }
        this.disconnectedByWifi = false;
      }.bind(this));
    }
  },

  handleWifiTetheringEnabled: function(enabled) {
    // Make sure Wifi is idle before switching to Wifi hotspot mode.
    if (enabled) {
      this.queueRequest({command: "setWifiEnabled", value: false}, function(data) {
        if (WifiManager.isWifiEnabled(WifiManager.state)) {
          this.disconnectedByWifiTethering = true;
          this._setWifiEnabled(false, this._setWifiEnabledCallback.bind(this));
        } else {
          this.requestDone();
        }
      }.bind(this));
    }

    this.queueRequest({command: "setWifiApEnabled", value: enabled}, function(data) {
      this.setWifiApEnabled(enabled, this.requestDone.bind(this));
    }.bind(this));

    if (!enabled) {
      this.queueRequest({command: "setWifiEnabled", value: true}, function(data) {
        if (this.disconnectedByWifiTethering) {
          this._setWifiEnabled(true, this._setWifiEnabledCallback.bind(this));
        } else {
          this.requestDone();
        }
        this.disconnectedByWifiTethering = false;
      }.bind(this));
    }
  },

  // nsIObserver implementation
  observe: function observe(subject, topic, data) {
    switch (topic) {
    case kMozSettingsChangedObserverTopic:
      // To avoid WifiWorker setting the wifi again, don't need to deal with
      // the "mozsettings-changed" event fired from internal setting.
      if ("wrappedJSObject" in subject) {
        subject = subject.wrappedJSObject;
      }
      if (subject.isInternalChange) {
        return;
      }

      this.handle(subject.key, subject.value);
      break;

    case "xpcom-shutdown":
      // Ensure the supplicant is detached from B2G to avoid XPCOM shutdown
      // blocks forever.
      WifiManager.ensureSupplicantDetached(() => {
        let wifiService = Cc["@mozilla.org/wifi/service;1"].getService(Ci.nsIWifiProxyService);
        wifiService.shutdown();
        let wifiCertService = Cc["@mozilla.org/wifi/certservice;1"].getService(Ci.nsIWifiCertService);
        wifiCertService.shutdown();
      });
      break;
    }
  },

  handle: function handle(aName, aResult) {
    switch(aName) {
      // TODO: Remove function call in Bug 1050147.
      case SETTINGS_WIFI_ENABLED:
        this.handleWifiEnabled(aResult)
        break;
      case SETTINGS_WIFI_DEBUG_ENABLED:
        if (aResult === null)
          aResult = false;
        DEBUG = aResult;
        updateDebug();
        break;
      case SETTINGS_WIFI_TETHERING_ENABLED:
        this._oldWifiTetheringEnabledState = this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED];
        // Fall through!
      case SETTINGS_WIFI_SSID:
      case SETTINGS_WIFI_SECURITY_TYPE:
      case SETTINGS_WIFI_SECURITY_PASSWORD:
      case SETTINGS_WIFI_IP:
      case SETTINGS_WIFI_PREFIX:
      case SETTINGS_WIFI_DHCPSERVER_STARTIP:
      case SETTINGS_WIFI_DHCPSERVER_ENDIP:
      case SETTINGS_WIFI_DNS1:
      case SETTINGS_WIFI_DNS2:
      case SETTINGS_USB_DHCPSERVER_STARTIP:
      case SETTINGS_USB_DHCPSERVER_ENDIP:
        // TODO: code related to wifi-tethering setting should be removed after GAIA
        //       use tethering API
        if (this.useTetheringAPI) {
          break;
        }

        if (aResult !== null) {
          this.tetheringSettings[aName] = aResult;
        }
        debug("'" + aName + "'" + " is now " + this.tetheringSettings[aName]);
        let index = this._wifiTetheringSettingsToRead.indexOf(aName);

        if (index != -1) {
          this._wifiTetheringSettingsToRead.splice(index, 1);
        }

        if (this._wifiTetheringSettingsToRead.length) {
          debug("We haven't read completely the wifi Tethering data from settings db.");
          break;
        }

        if (this._oldWifiTetheringEnabledState === this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED]) {
          debug("No changes for SETTINGS_WIFI_TETHERING_ENABLED flag. Nothing to do.");
          break;
        }

        if (this._oldWifiTetheringEnabledState === null &&
            !this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED]) {
          debug("Do nothing when initial settings for SETTINGS_WIFI_TETHERING_ENABLED flag is false.");
          break;
        }

        this._oldWifiTetheringEnabledState = this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED];
        this.handleWifiTetheringEnabled(aResult)
        break;
    };
  },

  handleError: function handleError(aErrorMessage) {
    debug("There was an error while reading Tethering settings.");
    this.tetheringSettings = {};
    this.tetheringSettings[SETTINGS_WIFI_TETHERING_ENABLED] = false;
  },
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiWorker]);

var debug;
function updateDebug() {
  if (DEBUG) {
    debug = function (s) {
      dump("-*- WifiWorker component: " + s + "\n");
    };
  } else {
    debug = function (s) {};
  }
  WifiManager.syncDebug();
}
updateDebug();