/* -*- 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/StateMachine.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/systemlibs.js");

XPCOMUtils.defineLazyServiceGetter(this, "gSysMsgr",
                                   "@mozilla.org/system-message-internal;1",
                                   "nsISystemMessagesInternal");

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

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

this.EXPORTED_SYMBOLS = ["WifiP2pManager"];

const EVENT_IGNORED                      = -1;
const EVENT_UNKNOWN                      = -2;

// Events from supplicant for p2p.
const EVENT_P2P_DEVICE_FOUND             = 0;
const EVENT_P2P_DEVICE_LOST              = 1;
const EVENT_P2P_GROUP_STARTED            = 2;
const EVENT_P2P_GROUP_REMOVED            = 3;
const EVENT_P2P_PROV_DISC_PBC_REQ        = 4;
const EVENT_P2P_PROV_DISC_PBC_RESP       = 5;
const EVENT_P2P_PROV_DISC_SHOW_PIN       = 6;
const EVENT_P2P_PROV_DISC_ENTER_PIN      = 7;
const EVENT_P2P_GO_NEG_REQUEST           = 8;
const EVENT_P2P_GO_NEG_SUCCESS           = 9;
const EVENT_P2P_GO_NEG_FAILURE           = 10;
const EVENT_P2P_GROUP_FORMATION_SUCCESS  = 11;
const EVENT_P2P_GROUP_FORMATION_FAILURE  = 12;
const EVENT_P2P_FIND_STOPPED             = 13;
const EVENT_P2P_INVITATION_RESULT        = 14;
const EVENT_P2P_INVITATION_RECEIVED      = 15;
const EVENT_P2P_PROV_DISC_FAILURE        = 16;

// Events from supplicant but not p2p specific.
const EVENT_AP_STA_DISCONNECTED          = 100;
const EVENT_AP_STA_CONNECTED             = 101;

// Events from DOM.
const EVENT_P2P_SET_PAIRING_CONFIRMATION = 1000;
const EVENT_P2P_CMD_CONNECT              = 1001;
const EVENT_P2P_CMD_DISCONNECT           = 1002;
const EVENT_P2P_CMD_ENABLE               = 1003;
const EVENT_P2P_CMD_DISABLE              = 1004;
const EVENT_P2P_CMD_ENABLE_SCAN          = 1005;
const EVENT_P2P_CMD_DISABLE_SCAN         = 1006;
const EVENT_P2P_CMD_BLOCK_SCAN           = 1007;
const EVENT_P2P_CMD_UNBLOCK_SCAN         = 1008;

// Internal events.
const EVENT_TIMEOUT_PAIRING_CONFIRMATION = 10000;
const EVENT_TIMEOUT_NEG_REQ              = 10001;
const EVENT_TIMEOUT_CONNECTING           = 10002;
const EVENT_P2P_ENABLE_SUCCESS           = 10003;
const EVENT_P2P_ENABLE_FAILED            = 10004;
const EVENT_P2P_DISABLE_SUCCESS          = 10005;

// WPS method string.
const WPS_METHOD_PBC     = "pbc";
const WPS_METHOD_DISPLAY = "display";
const WPS_METHOD_KEYPAD  = "keypad";

// Role string.
const P2P_ROLE_GO     = "GO";
const P2P_ROLE_CLIENT = "client";

// System message for pairing request.
const PAIRING_REQUEST_SYS_MSG = "wifip2p-pairing-request";

// Configuration.
const P2P_INTERFACE_NAME = "p2p0";
const DEFAULT_GO_INTENT = 15;
const DEFAULT_P2P_DEVICE_NAME = "FirefoxPhone";
const P2P_SCAN_TIMEOUT_SEC = 120;
const DEFAULT_P2P_WPS_METHODS = "virtual_push_button physical_display keypad"; // For wpa_supplicant.
const DEFAULT_P2P_DEVICE_TYPE = "10-0050F204-5"; // For wpa_supplicant.

const GO_NETWORK_INTERFACE = {
  ip:         "192.168.2.1",
  maskLength: 24,
  gateway:    "192.168.2.1",
  dns1:       "0.0.0.0",
  dns2:       "0.0.0.0",
  dhcpServer: "192.168.2.1"
};

const GO_DHCP_SERVER_IP_RANGE = {
  startIp: "192.168.2.10",
  endIp:   "192.168.2.30"
};

var gDebug = false;

// Device Capability bitmap
const DEVICE_CAPAB_SERVICE_DISCOVERY         = 1;
const DEVICE_CAPAB_CLIENT_DISCOVERABILITY    = 1<<1;
const DEVICE_CAPAB_CONCURRENT_OPER           = 1<<2;
const DEVICE_CAPAB_INFRA_MANAGED             = 1<<3;
const DEVICE_CAPAB_DEVICE_LIMIT              = 1<<4;
const DEVICE_CAPAB_INVITATION_PROCEDURE      = 1<<5;

// Group Capability bitmap
const GROUP_CAPAB_GROUP_OWNER                = 1;
const GROUP_CAPAB_PERSISTENT_GROUP           = 1<<1;
const GROUP_CAPAB_GROUP_LIMIT                = 1<<2;
const GROUP_CAPAB_INTRA_BSS_DIST             = 1<<3;
const GROUP_CAPAB_CROSS_CONN                 = 1<<4;
const GROUP_CAPAB_PERSISTENT_RECONN          = 1<<5;
const GROUP_CAPAB_GROUP_FORMATION            = 1<<6;

// Constants defined in wpa_supplicants.
const DEV_PW_REGISTRAR_SPECIFIED = 5;
const DEV_PW_USER_SPECIFIED      = 1;
const DEV_PW_PUSHBUTTON          = 4;

this.WifiP2pManager = function (aP2pCommand, aNetUtil) {
  function debug(aMsg) {
    if (gDebug) {
      dump('-------------- WifiP2pManager: ' + aMsg);
    }
  }

  let manager = {};

  let _stateMachine = P2pStateMachine(aP2pCommand, aNetUtil);

  // Set debug flag to true or false.
  //
  // @param aDebug Boolean to indicate enabling or disabling the debug flag.
  manager.setDebug = function(aDebug) {
    gDebug = aDebug;
  };

  // Set observer of observing internal state machine events.
  //
  // @param aObserver Used to notify WifiWorker what's happening
  //        in the internal p2p state machine.
  manager.setObserver = function(aObserver) {
    _stateMachine.setObserver(aObserver);
  };

  // Handle wpa_supplicant events.
  //
  // @param aEventString string from wpa_supplicant.
  manager.handleEvent = function(aEventString) {
    let event = parseEventString(aEventString);
    if (EVENT_UNKNOWN === event.id || EVENT_IGNORED === event.id) {
      debug('Unknow or ignored event: ' + aEventString);
      return false;
    }
    return _stateMachine.sendEvent(event);
  };

  // Set the confirmation of pairing request.
  //
  // @param aResult Object of confirmation result which contains:
  //    .accepted: user granted.
  //    .pin: pin code which is displaying or input by user.
  //    .wpsMethod: string of "pbc" or "display" or "keypad".
  manager.setPairingConfirmation = function(aResult) {
    let event = {
      id: EVENT_P2P_SET_PAIRING_CONFIRMATION,
      info: {
        accepted: aResult.accepted,
        pin: aResult.pin
      }
    };
    _stateMachine.sendEvent(event);
  };

  // Connect to a known peer.
  //
  // @param aAddress MAC address of the peer to connect.
  // @param aWpsMethod String of "pbc" or "display" or "keypad".
  // @param aGoIntent Number from 0 to 15.
  // @param aCallback Callback |true| on attempting to connect.
  //                          |false| on failed to connect.
  manager.connect = function(aAddress, aWpsMethod, aGoIntent, aCallback) {
    let event = {
      id: EVENT_P2P_CMD_CONNECT,
      info: {
        wpsMethod: aWpsMethod,
        address: aAddress,
        goIntent: aGoIntent,
        onDoConnect: aCallback
      }
    };
    _stateMachine.sendEvent(event);
  };

  // Disconnect with a known peer.
  //
  // @param aAddress The address the user desires to disconect.
  // @param aCallback Callback |true| on "attempting" to disconnect.
  //                           |false| on failed to disconnect.
  manager.disconnect = function(aAddress, aCallback) {
    let event = {
      id: EVENT_P2P_CMD_DISCONNECT,
      info: {
        address: aAddress,
        onDoDisconnect: aCallback
      }
    };
    _stateMachine.sendEvent(event);
  };

  // Enable/disable wifi p2p.
  //
  // @param aEnabled |true| to enable, |false| to disable.
  // @param aCallbacks object for callbacks:
  //   .onEnabled
  //   .onDisabled
  //   .onSupplicantConnected
  manager.setEnabled = function(aEnabled, aCallbacks) {
    let event = {
      id: (aEnabled ? EVENT_P2P_CMD_ENABLE : EVENT_P2P_CMD_DISABLE),
      info: {
        onEnabled: aCallbacks.onEnabled,
        onDisabled: aCallbacks.onDisabled,
        onSupplicantConnected: aCallbacks.onSupplicantConnected
      }
    };
    _stateMachine.sendEvent(event);
  };

  // Enable/disable the wifi p2p scan.
  //
  // @param aEnabled |true| to enable scan, |false| to disable scan.
  // @param aCallback Callback |true| on success to enable/disable scan.
  //                           |false| on failed to enable/disable scan.
  manager.setScanEnabled = function(aEnabled, aCallback) {
    let event = {
      id: (aEnabled ? EVENT_P2P_CMD_ENABLE_SCAN : EVENT_P2P_CMD_DISABLE_SCAN),
      info: { callback: aCallback }
    };
    _stateMachine.sendEvent(event);
  };

  // Block wifi p2p scan.
  manager.blockScan = function() {
    _stateMachine.sendEvent({ id: EVENT_P2P_CMD_BLOCK_SCAN });
  };

  // Un-block and do the pending scan if any.
  manager.unblockScan = function() {
    _stateMachine.sendEvent({ id: EVENT_P2P_CMD_UNBLOCK_SCAN });
  };

  // Set the p2p device name.
  manager.setDeviceName = function(newDeivceName, callback) {
    aP2pCommand.setDeviceName(newDeivceName, callback);
  };

  // Parse wps_supplicant event string.
  //
  // @param aEventString The raw event string from wpa_supplicant.
  //
  // @return Object:
  //   .id: a number to represent an event.
  //   .info: the additional information carried by this event string.
  function parseEventString(aEventString) {
    if (isIgnoredEvent(aEventString)) {
      return { id: EVENT_IGNORED };
    }

    let match = RegExp("p2p_dev_addr=([0-9a-fA-F:]+) " +
                       "pri_dev_type=([0-9a-zA-Z-]+) " +
                       "name='(.*)' " +
                       "config_methods=0x([0-9a-fA-F]+) " +
                       "dev_capab=0x([0-9a-fA-F]+) " +
                       "group_capab=0x([0-9a-fA-F]+) ").exec(aEventString + ' ');

    let tokens = aEventString.split(" ");

    let id = EVENT_UNKNOWN;

    // general info.
    let info = {};

    if (match) {
      info = {
        address:   match[1] ? match[1] : null,
        type:      match[2] ? match[2] : null,
        name:      match[3] ? match[3] : null,
        wpsFlag:   match[4] ? parseInt(match[4], 16) : null,
        devFlag:   match[5] ? parseInt(match[5], 16) : null,
        groupFlag: match[6] ? parseInt(match[6], 16) : null
      };
    }

    if (0 === aEventString.indexOf("P2P-DEVICE-FOUND")) {
      id = EVENT_P2P_DEVICE_FOUND;
      info.wpsCapabilities = wpsFlagToCapabilities(info.wpsFlag);
      info.isGroupOwner = isPeerGroupOwner(info.groupFlag);
    } else if (0 === aEventString.indexOf("P2P-DEVICE-LOST")) {
      // e.g. "P2P-DEVICE-LOST p2p_dev_addr=5e:0a:5b:15:1f:80".
      id = EVENT_P2P_DEVICE_LOST;
      info.address = /p2p_dev_addr=([0-9a-f:]+)/.exec(aEventString)[1];
    } else if (0 === aEventString.indexOf("P2P-GROUP-STARTED")) {
      // e.g. "P2P-GROUP-STARTED wlan0-p2p-0 GO ssid="DIRECT-3F Testing
      //       passphrase="12345678" go_dev_addr=02:40:61:c2:f3:b7 [PERSISTENT]".

      id = EVENT_P2P_GROUP_STARTED;
      let groupMatch = RegExp('ssid="(.*)" ' +
                              'freq=([0-9]*) ' +
                              '(passphrase|psk)=([^ ]+) ' +
                              'go_dev_addr=([0-9a-f:]+)').exec(aEventString);
      info.ssid = groupMatch[1];
      info.freq = groupMatch[2];
      if ('passphrase' === groupMatch[3]) {
        let s = groupMatch[4]; // e.g. "G7jHkkz9".
        info.passphrase = s.substring(1, s.length-1); // Trim the double quote.
      } else { // psk
        info.psk = groupMatch[4];
      }
      info.goAddress = groupMatch[5];
      info.ifname = tokens[1];
      info.role = tokens[2];
    } else if (0 === aEventString.indexOf("P2P-GROUP-REMOVED")) {
      id = EVENT_P2P_GROUP_REMOVED;
      // e.g. "P2P-GROUP-REMOVED wlan0-p2p-0 GO".
      info.ifname = tokens[1];
      info.role = tokens[2];
    } else if (0 === aEventString.indexOf("P2P-PROV-DISC-PBC-REQ")) {
      id = EVENT_P2P_PROV_DISC_PBC_REQ;
      info.wpsMethod = WPS_METHOD_PBC;
    } else if (0 === aEventString.indexOf("P2P-PROV-DISC-PBC-RESP")) {
      id = EVENT_P2P_PROV_DISC_PBC_RESP;
      // The address is different from the general pattern.
      info.address = aEventString.split(" ")[1];
      info.wpsMethod = WPS_METHOD_PBC;
    } else if (0 === aEventString.indexOf("P2P-PROV-DISC-SHOW-PIN")) {
      id = EVENT_P2P_PROV_DISC_SHOW_PIN;
      // Obtain peer address and pin from tokens.
      info.address = tokens[1];
      info.pin     = tokens[2];
      info.wpsMethod = WPS_METHOD_DISPLAY;
    } else if (0 === aEventString.indexOf("P2P-PROV-DISC-ENTER-PIN")) {
      id = EVENT_P2P_PROV_DISC_ENTER_PIN;
      // Obtain peer address from tokens.
      info.address = tokens[1];
      info.wpsMethod = WPS_METHOD_KEYPAD;
    } else if (0 === aEventString.indexOf("P2P-GO-NEG-REQUEST")) {
      id = EVENT_P2P_GO_NEG_REQUEST;
      info.address = tokens[1];
      switch (parseInt(tokens[2].split("=")[1], 10)) {
        case DEV_PW_REGISTRAR_SPECIFIED: // (5) Peer is display.
          info.wpsMethod = WPS_METHOD_KEYPAD;
          break;
        case DEV_PW_USER_SPECIFIED: // (1) Peer is keypad.
          info.wpsMethod = WPS_METHOD_DISPLAY;
          break;
        case DEV_PW_PUSHBUTTON: // (4) Peer is pbc.
          info.wpsMethod = WPS_METHOD_PBC;
          break;
        default:
          debug('Unknown wps method from event P2P-GO-NEG-REQUEST');
          break;
      }
    } else if (0 === aEventString.indexOf("P2P-GO-NEG-SUCCESS")) {
      id = EVENT_P2P_GO_NEG_SUCCESS;
    } else if (0 === aEventString.indexOf("P2P-GO-NEG-FAILURE")) {
      id = EVENT_P2P_GO_NEG_FAILURE;
    } else if (0 === aEventString.indexOf("P2P-GROUP-FORMATION-FAILURE")) {
      id = EVENT_P2P_GROUP_FORMATION_FAILURE;
    } else if (0 === aEventString.indexOf("P2P-GROUP-FORMATION-SUCCESS")) {
      id = EVENT_P2P_GROUP_FORMATION_SUCCESS;
    } else if (0 === aEventString.indexOf("P2P-FIND-STOPPED")) {
      id = EVENT_P2P_FIND_STOPPED;
    } else if (0 === aEventString.indexOf("P2P-INVITATION-RESULT")) {
      id = EVENT_P2P_INVITATION_RESULT;
      info.status = /status=([0-9]+)/.exec(aEventString)[1];
    } else if (0 === aEventString.indexOf("P2P-INVITATION-RECEIVED")) {
      // e.g. "P2P-INVITATION-RECEIVED sa=32:85:a9:da:e6:1f persistent=7".
      id = EVENT_P2P_INVITATION_RECEIVED;
      info.address = /sa=([0-9a-f:]+)/.exec(aEventString)[1];
      info.netId = /persistent=([0-9]+)/.exec(aEventString)[1];
    } else if (0 === aEventString.indexOf("P2P-PROV-DISC-FAILURE")) {
      id = EVENT_P2P_PROV_DISC_FAILURE;
    } else {
      // Not P2P event but we do receive it. Try to recognize it.
      if (0 === aEventString.indexOf("AP-STA-DISCONNECTED")) {
        id = EVENT_AP_STA_DISCONNECTED;
        info.address = tokens[1];
      } else if (0 === aEventString.indexOf("AP-STA-CONNECTED")) {
        id = EVENT_AP_STA_CONNECTED;
        info.address = tokens[1];
      } else {
        // Neither P2P event nor recognized supplicant event.
        debug('Unknwon event string: ' + aEventString);
      }
    }

    let event = {id: id, info: info};
    debug('Event parsing result: ' + aEventString + ": " + JSON.stringify(event));

    return event;
  }

  function isIgnoredEvent(aEventString) {
    const IGNORED_EVENTS = [
      "CTRL-EVENT-BSS-ADDED",
      "CTRL-EVENT-BSS-REMOVED",
      "CTRL-EVENT-SCAN-RESULTS",
      "CTRL-EVENT-STATE-CHANGE",
      "WPS-AP-AVAILABLE",
      "WPS-ENROLLEE-SEEN"
    ];
    for(let i = 0; i < IGNORED_EVENTS.length; i++) {
      if (0 === aEventString.indexOf(IGNORED_EVENTS[i])) {
        return true;
      }
    }
    return false;
  }

  function isPeerGroupOwner(aGroupFlag) {
    return (aGroupFlag & GROUP_CAPAB_GROUP_OWNER) !== 0;
  }

  // Convert flag to a wps capability array.
  //
  // @param aWpsFlag Number that represents the wps capabilities.
  // @return Array of WPS flag.
  function wpsFlagToCapabilities(aWpsFlag) {
    let wpsCapabilities = [];
    if (aWpsFlag & 0x8) {
      wpsCapabilities.push(WPS_METHOD_DISPLAY);
    }
    if (aWpsFlag & 0x80) {
      wpsCapabilities.push(WPS_METHOD_PBC);
    }
    if (aWpsFlag & 0x100) {
      wpsCapabilities.push(WPS_METHOD_KEYPAD);
    }
    return wpsCapabilities;
  }

  _stateMachine.start();
  return manager;
};

function P2pStateMachine(aP2pCommand, aNetUtil) {
  function debug(aMsg) {
    if (gDebug) {
      dump('-------------- WifiP2pStateMachine: ' + aMsg);
    }
  }

  let p2pSm = {};  // The state machine to return.

  let _sm = StateMachine('WIFIP2P'); // The general purpose state machine.

  // Information we need to keep track across states.
  let _observer;

  let _onEnabled;
  let _onDisabled;
  let _onSupplicantConnected;
  let _savedConfig = {}; // Configuration used to do P2P_CONNECT.
  let _groupInfo = {};   // The information of the group we have formed.
  let _removedGroupInfo = {}; // Used to store the group info we are going to remove.

  let _scanBlocked = false;
  let _scanPostponded = false;

  let _localDevice = {
    address: "",
    deviceName: DEFAULT_P2P_DEVICE_NAME + "_" + libcutils.property_get("ro.build.product"),
    wpsCapabilities: [WPS_METHOD_PBC, WPS_METHOD_KEYPAD, WPS_METHOD_DISPLAY]
  };

  let _p2pNetworkInterface = {
    QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface]),

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

      state: Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED,
      type: Ci.nsINetworkInfo.NETWORK_TYPE_WIFI_P2P,
      name: P2P_INTERFACE_NAME,
      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,

    // help
    registered: false
  };

  //---------------------------------------------------------
  // State machine APIs.
  //---------------------------------------------------------

  // Register the observer which is implemented in WifiP2pWorkerObserver.jsm.
  //
  // @param aObserver:
  //   .onEnabled
  //   .onDisbaled
  //   .onPeerFound
  //   .onPeerLost
  //   .onConnecting
  //   .onConnected
  //   .onDisconnected
  //   .onLocalDeviceChanged
  p2pSm.setObserver = function(aObserver) {
    _observer = aObserver;
  };

  p2pSm.start = function() {
    _sm.start(stateDisabled);
  };

  p2pSm.sendEvent = function(aEvent) {
    let willBeHandled = isInP2pManagedState(_sm.getCurrentState());
    _sm.sendEvent(aEvent);
    return willBeHandled;
  };

  // Initialize internal state machine _sm.
  _sm.setDefaultEventHandler(handleEventCommon);

  //----------------------------------------------------------
  // State definition.
  //----------------------------------------------------------

  // The initial state.
  var stateDisabled = _sm.makeState("DISABLED", {
    enter: function() {
      _onEnabled = null;
      _onSupplicantConnected = null;
      _savedConfig = null;
      _groupInfo = null;
      _removedGroupInfo = null;
      _scanBlocked = false;
      _scanPostponded = false;

      unregisterP2pNetworkInteface();
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_CMD_ENABLE:
          _onEnabled = aEvent.info.onEnabled;
          _onSupplicantConnected = aEvent.info.onSupplicantConnected;
          _sm.gotoState(stateEnabling);
          break;

        default:
          return false;
      } // End of switch.
      return true;
    }
  });

  // The state where we are trying to enable wifi p2p.
  var stateEnabling = _sm.makeState("ENABLING", {
    enter: function() {

      function onFailure()
      {
        _onEnabled(false);
        _observer.onDisabled();
        _sm.gotoState(stateDisabled);
      }

      function onSuccess()
      {
        _onEnabled(true);
        _observer.onEnabled();
        _sm.gotoState(stateInactive);
      }

      _sm.pause();

      // This function will only call back on success.
      function connectToSupplicantIfNeeded(callback) {
        if (aP2pCommand.getSdkVersion() >= 19) {
          // No need to connect to supplicant on KK. Call back directly.
          callback();
          return;
        }
        aP2pCommand.connectToSupplicant(function (status) {
          if (0 !== status) {
            debug('Failed to connect to p2p0');
            onFailure();
            return;
          }
          debug('wpa_supplicant p2p0 connected!');
          _onSupplicantConnected();
          callback();
        });
      }

      // Step 1: Connect to p2p0 if needed.
      connectToSupplicantIfNeeded(function callback () {
        let detail;

        // Step 2: Get MAC address.
        if (!_localDevice.address) {
          aP2pCommand.getMacAddress(function (address) {
            if (!address) {
              debug('Failed to get MAC address....');
              onFailure();
              return;
            }
            debug('Got mac address: ' + address);
            _localDevice.address = address;
            _observer.onLocalDeviceChanged(_localDevice);
          });
        }

        // Step 3: Enable p2p with the device name and wps methods.
        detail = { deviceName: _localDevice.deviceName,
                   deviceType: libcutils.property_get("ro.moz.wifi.p2p_device_type") || DEFAULT_P2P_DEVICE_TYPE,
                   wpsMethods: libcutils.property_get("ro.moz.wifi.p2p_wps_methods") || DEFAULT_P2P_WPS_METHODS };

        aP2pCommand.p2pEnable(detail, function (success) {
          if (!success) {
            debug('Failed to enable p2p');
            onFailure();
            return;
          }

          debug('P2P is enabled! Enabling net interface...');

          // Step 4: Enable p2p0 net interface. wpa_supplicant may have
          //         already done it for us.
          gNetworkService.enableInterface(P2P_INTERFACE_NAME, function (success) {
            onSuccess();
          });
        });
      });
    },

    handleEvent: function(aEvent) {
      // We won't receive any event since all of them will be blocked.
      return true;
    }
  });

  // The state just after enabling wifi direct.
  var stateInactive = _sm.makeState("INACTIVE", {
    enter: function() {
      registerP2pNetworkInteface();

      if (_sm.getPreviousState() !== stateEnabling) {
        _observer.onDisconnected(_savedConfig);
      }

      _savedConfig = null; // Used to connect p2p peer.
      _groupInfo   = null; // The information of the formed group.
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        // Receiving the following 3 states implies someone is trying to
        // connect to me.
        case EVENT_P2P_PROV_DISC_PBC_REQ:
        case EVENT_P2P_PROV_DISC_SHOW_PIN:
        case EVENT_P2P_PROV_DISC_ENTER_PIN:
          debug('Someone is trying to connect to me: ' + JSON.stringify(aEvent.info));

          _savedConfig = {
            name:      aEvent.info.name,
            address:   aEvent.info.address,
            wpsMethod: aEvent.info.wpsMethod,
            goIntent:  DEFAULT_GO_INTENT,
            pin:       aEvent.info.pin // EVENT_P2P_PROV_DISC_SHOW_PIN only.
          };

          _sm.gotoState(stateWaitingForConfirmation);
          break;

        // Connect to a peer.
        case EVENT_P2P_CMD_CONNECT:
          debug('Trying to connect to peer: ' + JSON.stringify(aEvent.info));

          _savedConfig = {
            address:   aEvent.info.address,
            wpsMethod: aEvent.info.wpsMethod,
            goIntent:  aEvent.info.goIntent
          };

          _sm.gotoState(stateProvisionDiscovery);
          aEvent.info.onDoConnect(true);
          break;

        case EVENT_P2P_INVITATION_RECEIVED:
          _savedConfig = {
            address: aEvent.info.address,
            wpsMethod: WPS_METHOD_PBC,
            goIntent: DEFAULT_GO_INTENT,
            netId: aEvent.info.netId
          };
          _sm.gotoState(stateWaitingForInvitationConfirmation);
          break;

        case EVENT_P2P_GROUP_STARTED:
          // Most likely the peer just reinvoked a peristen group and succeeeded.

          _savedConfig = { address: aEvent.info.goAddress };

          _sm.pause();
          handleGroupStarted(aEvent.info, function (success) {
            _sm.resume();
          });
          break;

        case EVENT_AP_STA_DISCONNECTED:
          // We will hit this case when we used to be a group owner and
          // requested to remove the group we owned.
          break;

        default:
          return false;
      } // End of switch.
      return true;
    },
  });

  // Waiting for user's confirmation.
  var stateWaitingForConfirmation = _sm.makeState("WAITING_FOR_CONFIRMATION", {
    timeoutTimer: null,

    enter: function() {
      gSysMsgr.broadcastMessage(PAIRING_REQUEST_SYS_MSG, _savedConfig);
      this.timeoutTimer = initTimeoutTimer(30000, EVENT_TIMEOUT_PAIRING_CONFIRMATION);
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_SET_PAIRING_CONFIRMATION:
          if (!aEvent.info.accepted) {
            debug('User rejected this request');
            _sm.gotoState(stateInactive); // Reset to inactive state.
            break;
          }

          debug('User accepted this request');

          // The only information we may have to grab from user.
          _savedConfig.pin = aEvent.info.pin;

          // The case that user requested to form a group ealier on.
          // Just go to connecting state and do p2p_connect.
          if (_sm.getPreviousState() === stateProvisionDiscovery) {
            _sm.gotoState(stateConnecting);
            break;
          }

          // Otherwise, wait for EVENT_P2P_GO_NEG_REQUEST.
          _sm.gotoState(stateWaitingForNegReq);
          break;

        case EVENT_TIMEOUT_PAIRING_CONFIRMATION:
          debug('Confirmation timeout!');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GO_NEG_REQUEST:
          _sm.deferEvent(aEvent);
          break;

        default:
          return false;
      } // End of switch.

      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
      this.timeoutTimer = null;
    }
  });

  var stateWaitingForNegReq = _sm.makeState("WAITING_FOR_NEG_REQ", {
    timeoutTimer: null,

    enter: function() {
      debug('Wait for EVENT_P2P_GO_NEG_REQUEST');
      this.timeoutTimer = initTimeoutTimer(30000, EVENT_TIMEOUT_NEG_REQ);
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_GO_NEG_REQUEST:
          if (aEvent.info.wpsMethod !== _savedConfig.wpsMethod) {
            debug('Unmatched wps method: ' + aEvent.info.wpsMethod + ", " + _savedConfig.wpsMetho);
          }
          _sm.gotoState(stateConnecting);
          break;

        case EVENT_TIMEOUT_NEG_REQ:
          debug("Waiting for NEG-REQ timeout");
          _sm.gotoState(stateInactive);
          break;

        default:
          return false;
      } // End of switch.
      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
      this.timeoutTimer = null;
    }
  });

  // Waiting for user's confirmation for invitation.
  var stateWaitingForInvitationConfirmation = _sm.makeState("WAITING_FOR_INV_CONFIRMATION", {
    timeoutTimer: null,

    enter: function() {
      gSysMsgr.broadcastMessage(PAIRING_REQUEST_SYS_MSG, _savedConfig);
      this.timeoutTimer = initTimeoutTimer(30000, EVENT_TIMEOUT_PAIRING_CONFIRMATION);
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_SET_PAIRING_CONFIRMATION:
          if (!aEvent.info.accepted) {
            debug('User rejected this request');
            _sm.gotoState(stateInactive); // Reset to inactive state.
            break;
          }

          debug('User accepted this request');
          _sm.pause();
          aP2pCommand.p2pGetGroupCapab(_savedConfig.address, function (gc) {
            let isPeeGroupOwner = gc & GROUP_CAPAB_GROUP_OWNER;
            _sm.gotoState(isPeeGroupOwner ? stateGroupAdding : stateReinvoking);
          });

          break;

        case EVENT_TIMEOUT_PAIRING_CONFIRMATION:
          debug('Confirmation timeout!');
          _sm.gotoState(stateInactive);
          break;

        default:
          return false;
      } // End of switch.

      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
      this.timeoutTimer = null;
    }
  });

  var stateGroupAdding = _sm.makeState("GROUP_ADDING", {
    timeoutTimer: null,

    enter: function() {
      let self = this;

      _observer.onConnecting(_savedConfig);

      _sm.pause();
      aP2pCommand.p2pGroupAdd(_savedConfig.netId, function (success) {
        if (!success) {
          _sm.gotoState(stateInactive);
          return;
        }
        // Waiting for EVENT_P2P_GROUP_STARTED.
        self.timeoutTimer = initTimeoutTimer(60000, EVENT_TIMEOUT_CONNECTING);
        _sm.resume();
      });
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_GROUP_STARTED:
          _sm.pause();
          handleGroupStarted(aEvent.info, function (success) {
            _sm.resume();
          });
          break;

        case EVENT_P2P_GO_NEG_FAILURE:
          debug('Negotiation failure. Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_TIMEOUT_CONNECTING:
          debug('Connecting timeout! Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_FORMATION_SUCCESS:
        case EVENT_P2P_GO_NEG_SUCCESS:
          break;

        case EVENT_P2P_GROUP_FORMATION_FAILURE:
          debug('Group formation failure');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_REMOVED:
          debug('Received P2P-GROUP-REMOVED due to previous failed handleGroupdStarted()');
          _removedGroupInfo = {
            role:   aEvent.info.role,
            ifname: aEvent.info.ifname
          };
          _sm.gotoState(stateDisconnecting);
          break;

        default:
          return false;
      } // End of switch.

      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
      this.timeoutTimer = null;
    }
  });

  var stateReinvoking = _sm.makeState("REINVOKING", {
    timeoutTimer: null,

    enter: function() {
      let self = this;

      _observer.onConnecting(_savedConfig);
      _sm.pause();
      aP2pCommand.p2pReinvoke(_savedConfig.netId, _savedConfig.address, function(success) {
        if (!success) {
          _sm.gotoState(stateInactive);
          return;
        }
        // Waiting for EVENT_P2P_GROUP_STARTED.
        self.timeoutTimer = initTimeoutTimer(60000, EVENT_TIMEOUT_CONNECTING);
        _sm.resume();
      });
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_GROUP_STARTED:
          _sm.pause();
          handleGroupStarted(aEvent.info, function(success) {
            _sm.resume();
          });
          break;

        case EVENT_P2P_GO_NEG_FAILURE:
          debug('Negotiation failure. Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_TIMEOUT_CONNECTING:
          debug('Connecting timeout! Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_FORMATION_SUCCESS:
        case EVENT_P2P_GO_NEG_SUCCESS:
          break;

        case EVENT_P2P_GROUP_FORMATION_FAILURE:
          debug('Group formation failure');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_REMOVED:
          debug('Received P2P-GROUP-REMOVED due to previous failed handleGroupdStarted()');
          _removedGroupInfo = {
            role:   aEvent.info.role,
            ifname: aEvent.info.ifname
          };
          _sm.gotoState(stateDisconnecting);
          break;

        default:
          return false;
      } // End of switch.

      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
    }
  });

  var stateProvisionDiscovery = _sm.makeState("PROVISION_DISCOVERY", {
    enter: function() {
      function onDiscoveryCommandSent(success) {
        if (!success) {
          _sm.gotoState(stateInactive);
          debug('Failed to send p2p_prov_disc. Go back to inactive state.');
          return;
        }

        debug('p2p_prov_disc has been sent.');

        _sm.resume();
        // Waiting for EVENT_P2P_PROV_DISC_PBC_RESP or
        //             EVENT_P2P_PROV_DISC_SHOW_PIN or
        //             EVENT_P2P_PROV_DISC_ENTER_PIN.
      }

      _sm.pause();
      aP2pCommand.p2pProvDiscovery(_savedConfig.address,
                                   toPeerWpsMethod(_savedConfig.wpsMethod),
                                   onDiscoveryCommandSent);
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_PROV_DISC_PBC_RESP:
          _sm.gotoState(stateConnecting); // No need for local user grant.
          break;
        case EVENT_P2P_PROV_DISC_SHOW_PIN:
        case EVENT_P2P_PROV_DISC_ENTER_PIN:
          if (aEvent.info.wpsMethod !== _savedConfig.wpsMethod) {
            debug('Unmatched wps method: ' + aEvent.info.wpsMethod + ":" + _savedConfig.wpsMethod);
          }
          if (EVENT_P2P_PROV_DISC_SHOW_PIN === aEvent.id) {
            _savedConfig.pin = aEvent.info.pin;
          }
          _sm.gotoState(stateWaitingForConfirmation);
          break;

        case EVENT_P2P_PROV_DISC_FAILURE:
          _sm.gotoState(stateInactive);
          break;

        default:
          return false;
      } // End of switch.
      return true;
    }
  });

  // We are going to connect to the peer.
  // |_savedConfig| is supposed to have been filled properly.
  var stateConnecting = _sm.makeState("CONNECTING", {
    timeoutTimer: null,

    enter: function() {
      let self = this;

      if (null === _savedConfig.goIntent) {
        _savedConfig.goIntent = DEFAULT_GO_INTENT;
      }

      _observer.onConnecting(_savedConfig);

      let wpsMethodWithPin;
      if (WPS_METHOD_KEYPAD === _savedConfig.wpsMethod ||
          WPS_METHOD_DISPLAY === _savedConfig.wpsMethod) {
        // e.g. '12345678 display or '12345678 keypad'.
        wpsMethodWithPin = (_savedConfig.pin + ' ' + _savedConfig.wpsMethod);
      } else {
        // e.g. 'pbc'.
        wpsMethodWithPin = _savedConfig.wpsMethod;
      }

      _sm.pause();

      aP2pCommand.p2pGetGroupCapab(_savedConfig.address, function(gc) {
        debug('group capabilities of ' + _savedConfig.address + ': ' + gc);

        let isPeerGroupOwner = gc & GROUP_CAPAB_GROUP_OWNER;
        let config = { address:           _savedConfig.address,
                       wpsMethodWithPin:  wpsMethodWithPin,
                       goIntent:          _savedConfig.goIntent,
                       joinExistingGroup: isPeerGroupOwner };

        aP2pCommand.p2pConnect(config, function (success) {
          if (!success) {
            debug('Failed to send p2p_connect');
            _sm.gotoState(stateInactive);
            return;
          }
          debug('Waiting for EVENT_P2P_GROUP_STARTED.');
          self.timeoutTimer = initTimeoutTimer(60000, EVENT_TIMEOUT_CONNECTING);
          _sm.resume();
        });
      });
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_GROUP_STARTED:
          _sm.pause();
          handleGroupStarted(aEvent.info, function (success) {
            _sm.resume();
          });
          break;

        case EVENT_P2P_GO_NEG_FAILURE:
          debug('Negotiation failure. Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_TIMEOUT_CONNECTING:
          debug('Connecting timeout! Go back to inactive state');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_FORMATION_SUCCESS:
        case EVENT_P2P_GO_NEG_SUCCESS:
          break;

        case EVENT_P2P_GROUP_FORMATION_FAILURE:
          debug('Group formation failure');
          _sm.gotoState(stateInactive);
          break;

        case EVENT_P2P_GROUP_REMOVED:
          debug('Received P2P-GROUP-REMOVED due to previous failed ' +
                'handleGroupdStarted()');
          _removedGroupInfo = {
            role:   aEvent.info.role,
            ifname: aEvent.info.ifname
          };
          _sm.gotoState(stateDisconnecting);
          break;

        default:
          return false;
      } // End of switch.

      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
    }
  });

  var stateConnected = _sm.makeState("CONNECTED", {
    groupOwner: null,

    enter: function() {
      this.groupOwner = {
        macAddress: _groupInfo.goAddress,
        ipAddress:  _groupInfo.networkInterface.info.gateways[0],
        passphrase: _groupInfo.passphrase,
        ssid:       _groupInfo.ssid,
        freq:       _groupInfo.freq,
        isLocal:    _groupInfo.isGroupOwner
      };

      if (!_groupInfo.isGroupOwner) {
        _observer.onConnected(this.groupOwner, _savedConfig);
      } else {
        // If I am a group owner, notify onConnected until EVENT_AP_STA_CONNECTED
        // is received.
      }

      _removedGroupInfo = null;
    },

    handleEvent: function(aEvent) {
      switch (aEvent.id) {
        case EVENT_AP_STA_CONNECTED:
          if (_groupInfo.isGroupOwner) {
            _observer.onConnected(this.groupOwner, _savedConfig);
          }
          break;

        case EVENT_P2P_GROUP_REMOVED:
          _removedGroupInfo = {
            role:   aEvent.info.role,
            ifname: aEvent.info.ifname
          };
          _sm.gotoState(stateDisconnecting);
          break;

        case EVENT_AP_STA_DISCONNECTED:
          debug('Client disconnected: ' + aEvent.info.address);

          // Now we suppose it's the only client. Remove my group.
          _sm.pause();
          aP2pCommand.p2pGroupRemove(_groupInfo.ifname, function (success) {
            debug('Requested to remove p2p group. Wait for EVENT_P2P_GROUP_REMOVED.');
            _sm.resume();
          });
          break;

        case EVENT_P2P_CMD_DISCONNECT:
          // Since we only support single connection, we can ignore
          // the given peer address.
          _sm.pause();
          aP2pCommand.p2pGroupRemove(_groupInfo.ifname, function(success) {
            aEvent.info.onDoDisconnect(true);
            _sm.resume();
          });

          debug('Sent disconnect command. Wait for EVENT_P2P_GROUP_REMOVED.');
          break;

        case EVENT_P2P_PROV_DISC_PBC_REQ:
        case EVENT_P2P_PROV_DISC_SHOW_PIN:
        case EVENT_P2P_PROV_DISC_ENTER_PIN:
          debug('Someone is trying to connect to me: ' + JSON.stringify(aEvent.info));

          _savedConfig = {
            name:      aEvent.info.name,
            address:   aEvent.info.address,
            wpsMethod: aEvent.info.wpsMethod,
            pin:       aEvent.info.pin
          };

          _sm.gotoState(stateWaitingForJoiningConfirmation);
          break;

        default:
          return false;
      } // end of switch
      return true;
    }
  });

  var stateWaitingForJoiningConfirmation = _sm.makeState("WAITING_FOR_JOINING_CONFIRMATION", {
    timeoutTimer: null,

    enter: function() {
      gSysMsgr.broadcastMessage(PAIRING_REQUEST_SYS_MSG, _savedConfig);
      this.timeoutTimer = initTimeoutTimer(30000, EVENT_TIMEOUT_PAIRING_CONFIRMATION);
    },

    handleEvent: function (aEvent) {
      switch (aEvent.id) {
        case EVENT_P2P_SET_PAIRING_CONFIRMATION:
          if (!aEvent.info.accepted) {
            debug('User rejected invitation!');
            _sm.gotoState(stateConnected);
            break;
          }

          let onWpsCommandSent = function(success) {
            _observer.onConnecting(_savedConfig);
            _sm.gotoState(stateConnected);
          };

          _sm.pause();
          if (WPS_METHOD_PBC === _savedConfig.wpsMethod) {
            aP2pCommand.wpsPbc(onWpsCommandSent, _groupInfo.ifname);
          } else {
            let detail = { pin: _savedConfig.pin, iface: _groupInfo.ifname };
            aP2pCommand.wpsPin(detail, onWpsCommandSent);
          }
          break;

        case EVENT_TIMEOUT_PAIRING_CONFIRMATION:
          debug('WAITING_FOR_JOINING_CONFIRMATION timeout!');
          _sm.gotoState(stateConnected);
          break;

        default:
          return false;
      } // End of switch.
      return true;
    },

    exit: function() {
      this.timeoutTimer.cancel();
      this.timeoutTimer = null;
    }
  });

  var stateDisconnecting = _sm.makeState("DISCONNECTING", {
    enter: function() {
      _sm.pause();
      handleGroupRemoved(_removedGroupInfo, function (success) {
        if (!success) {
          debug('Failed to handle group removed event. What can I do?');
        }
        _sm.gotoState(stateInactive);
      });
    },

    handleEvent: function(aEvent) {
      return false; // We will not receive any event in this state.
    }
  });

  var stateDisabling = _sm.makeState("DISABLING", {
    enter: function() {
      _sm.pause();
      aNetUtil.stopDhcpServer(function (success) { // Stopping DHCP server is harmless.
        debug('Stop DHCP server result: ' + success);
        aP2pCommand.p2pDisable(function(success) {
          debug('P2P function disabled');
          closeSupplicantConnectionIfNeeded(function() {
            debug('Supplicant connection closed');
            gNetworkService.disableInterface(P2P_INTERFACE_NAME, function (success){
              debug('Disabled interface: ' + P2P_INTERFACE_NAME);
              _onDisabled(true);
              _observer.onDisabled();
              _sm.gotoState(stateDisabled);
            });
          });
        });
      });

      function closeSupplicantConnectionIfNeeded(callback) {
        // No need to connect to supplicant on KK. Call back directly.
        if (aP2pCommand.getSdkVersion() >= 19) {
          callback();
          return;
        }
        aP2pCommand.closeSupplicantConnection(callback);
      }
    },

    handleEvent: function(aEvent) {
      return false; // We will not receive any event in this state.
    }
  });

  //----------------------------------------------------------
  // Helper functions.
  //----------------------------------------------------------

  // Handle 'P2P_GROUP_STARTED' event. Note that this function
  // will also do the state transitioning and error handling.
  //
  // @param aInfo Information carried by "P2P_GROUP_STARTED" event:
  //   .role: P2P_ROLE_GO or P2P_ROLE_CLIENT
  //   .ssid:
  //   .freq:
  //   .passphrase: Used to connect to GO for legacy device.
  //   .goAddress:
  //   .ifname: e.g. p2p-p2p0
  //
  // @param aCallback Callback function.
  function handleGroupStarted(aInfo, aCallback) {
    debug('handleGroupStarted: ' + JSON.stringify(aInfo));

    function onSuccess()
    {
      _sm.gotoState(stateConnected);
      aCallback(true);
    }

    function onFailure()
    {
      debug('Failed to handleGroupdStarted(). Remove the group...');
      aP2pCommand.p2pGroupRemove(aInfo.ifname, function (success) {
        aCallback(false);

        if (success) {
          return; // Stay in current state and wait for EVENT_P2P_GROUP_REMOVED.
        }

        debug('p2pGroupRemove command error!');
        _sm.gotoState(stateInactive);
      });
    }

    // Save this group information.
    _groupInfo = aInfo;
    _groupInfo.isGroupOwner = (P2P_ROLE_GO === aInfo.role);

    if (_groupInfo.isGroupOwner) {
      debug('Group owner. Start DHCP server');
      let dhcpServerConfig = { ifname: aInfo.ifname,
                               startIp: GO_DHCP_SERVER_IP_RANGE.startIp,
                               endIp: GO_DHCP_SERVER_IP_RANGE.endIp,
                               serverIp: GO_NETWORK_INTERFACE.ip,
                               maskLength: GO_NETWORK_INTERFACE.maskLength };

      aNetUtil.startDhcpServer(dhcpServerConfig, function (success) {
        if (!success) {
          debug('Failed to start DHCP server');
          onFailure();
          return;
        }

        // Update p2p network interface.
        _p2pNetworkInterface.info.state = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED;
        _p2pNetworkInterface.info.ips = [GO_NETWORK_INTERFACE.ip];
        _p2pNetworkInterface.info.prefixLengths = [GO_NETWORK_INTERFACE.maskLength];
        _p2pNetworkInterface.info.gateways = [GO_NETWORK_INTERFACE.ip];
        handleP2pNetworkInterfaceStateChanged();

        _groupInfo.networkInterface = _p2pNetworkInterface;

        debug('Everything is done. Happy p2p GO~');
        onSuccess();
      });

      return;
    }

    // We are the client.

    debug("Client. Request IP from DHCP server on interface: " + _groupInfo.ifname);

    aNetUtil.runDhcp(aInfo.ifname, 0, function(dhcpData) {
      if(!dhcpData || !dhcpData.info) {
        debug('Failed to run DHCP client');
        onFailure();
        return;
      }

      // Save network interface.
      debug("DHCP request success: " + JSON.stringify(dhcpData.info));

      // Update p2p network interface.
      let maskLength =
        netHelpers.getMaskLength(netHelpers.stringToIP(dhcpData.info.mask_str));
      if (!maskLength) {
        maskLength = 32; // max prefix for IPv4.
      }
      _p2pNetworkInterface.info.state = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED;
      _p2pNetworkInterface.info.ips = [dhcpData.info.ipaddr_str];
      _p2pNetworkInterface.info.prefixLengths = [maskLength];
      if (typeof dhcpData.info.dns1_str == "string" &&
          dhcpData.info.dns1_str.length) {
        _p2pNetworkInterface.info.dnses.push(dhcpData.info.dns1_str);
      }
      if (typeof dhcpData.info.dns2_str == "string" &&
          dhcpData.info.dns2_str.length) {
        _p2pNetworkInterface.info.dnses.push(dhcpData.info.dns2_str);
      }
      _p2pNetworkInterface.info.gateways = [dhcpData.info.gateway_str];
      handleP2pNetworkInterfaceStateChanged();

      _groupInfo.networkInterface = _p2pNetworkInterface;

      debug('Happy p2p client~');
      onSuccess();
    });
  }

  function resetP2pNetworkInterface() {
    _p2pNetworkInterface.info.state = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED;
    _p2pNetworkInterface.info.ips = [];
    _p2pNetworkInterface.info.prefixLengths = [];
    _p2pNetworkInterface.info.dnses = [];
    _p2pNetworkInterface.info.gateways = [];
  }

  function registerP2pNetworkInteface() {
    if (!_p2pNetworkInterface.registered) {
      resetP2pNetworkInterface();
      gNetworkManager.registerNetworkInterface(_p2pNetworkInterface);
      _p2pNetworkInterface.registered = true;
    }
  }

  function unregisterP2pNetworkInteface() {
    if (_p2pNetworkInterface.registered) {
      resetP2pNetworkInterface();
      gNetworkManager.unregisterNetworkInterface(_p2pNetworkInterface);
      _p2pNetworkInterface.registered = false;
    }
  }

  function handleP2pNetworkInterfaceStateChanged() {
    gNetworkManager.updateNetworkInterface(_p2pNetworkInterface);
  }

  // Handle 'P2P_GROUP_STARTED' event.
  //
  // @param aInfo information carried by "P2P_GROUP_REMOVED" event:
  //   .ifname
  //   .role: "GO" or "client".
  //
  // @param aCallback Callback function.
  function handleGroupRemoved(aInfo, aCallback) {
    if (!_groupInfo) {
      debug('No group info. Why?');
      aCallback(true);
      return;
    }
    if (_groupInfo.ifname !== aInfo.ifname ||
        _groupInfo.role   !== aInfo.role) {
      debug('Unmatched group info: ' + JSON.stringify(_groupInfo) +
            ' v.s. ' + JSON.stringify(aInfo));
    }

    // Update p2p network interface.
    _p2pNetworkInterface.info.state = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED;
    handleP2pNetworkInterfaceStateChanged();

    if (P2P_ROLE_GO === aInfo.role) {
      aNetUtil.stopDhcpServer(function(success) {
        debug('Stop DHCP server result: ' + success);
        aCallback(true);
      });
    } else {
      aNetUtil.stopDhcp(aInfo.ifname, function() {
        aCallback(true);
      });
    }
  }

  // Non state-specific event handler.
  function handleEventCommon(aEvent) {
    switch (aEvent.id) {
      case EVENT_P2P_DEVICE_FOUND:
        _observer.onPeerFound(aEvent.info);
        break;

      case EVENT_P2P_DEVICE_LOST:
        _observer.onPeerLost(aEvent.info);
        break;

      case EVENT_P2P_CMD_DISABLE:
        _onDisabled = aEvent.info.onDisabled;
        _sm.gotoState(stateDisabling);
        break;

      case EVENT_P2P_CMD_ENABLE_SCAN:
        if (_scanBlocked) {
          _scanPostponded = true;
          aEvent.info.callback(true);
          break;
        }
        aP2pCommand.p2pEnableScan(P2P_SCAN_TIMEOUT_SEC, aEvent.info.callback);
        break;

      case EVENT_P2P_CMD_DISABLE_SCAN:
        aP2pCommand.p2pDisableScan(aEvent.info.callback);
        break;

      case EVENT_P2P_FIND_STOPPED:
        break;

      case EVENT_P2P_CMD_BLOCK_SCAN:
        _scanBlocked = true;
        aP2pCommand.p2pDisableScan(function(success) {});
        break;

      case EVENT_P2P_CMD_UNBLOCK_SCAN:
        _scanBlocked = false;
        if (_scanPostponded) {
          aP2pCommand.p2pEnableScan(P2P_SCAN_TIMEOUT_SEC, function(success) {});
        }
        break;

      case EVENT_P2P_CMD_CONNECT:
      case EVENT_P2P_CMD_DISCONNECT:
        debug("The current state couldn't handle connect/disconnect request. Ignore it.");
        break;

      default:
        return false;
    } // End of switch.
    return true;
  }

  function isInP2pManagedState(aState) {
    let p2pManagedStates = [stateWaitingForConfirmation,
                            stateWaitingForNegReq,
                            stateProvisionDiscovery,
                            stateWaitingForInvitationConfirmation,
                            stateGroupAdding,
                            stateReinvoking,
                            stateConnecting,
                            stateConnected,
                            stateDisconnecting];

    for (let i = 0; i < p2pManagedStates.length; i++) {
      if (aState === p2pManagedStates[i]) {
        return true;
      }
    }

    return false;
  }

  function initTimeoutTimer(aTimeoutMs, aTimeoutEvent) {
    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    function onTimerFired() {
      _sm.sendEvent({ id: aTimeoutEvent });
      timer = null;
    }
    timer.initWithCallback(onTimerFired.bind(this), aTimeoutMs,
                           Ci.nsITimer.TYPE_ONE_SHOT);
    return timer;
  }

  // Converts local WPS method to peer WPS method.
  function toPeerWpsMethod(aLocalWpsMethod) {
    switch (aLocalWpsMethod) {
      case WPS_METHOD_DISPLAY:
        return WPS_METHOD_KEYPAD;
      case WPS_METHOD_KEYPAD:
        return WPS_METHOD_DISPLAY;
      case WPS_METHOD_PBC:
        return WPS_METHOD_PBC;
      default:
        return WPS_METHOD_PBC; // Use "push button" as the default method.
    }
  }

  return p2pSm;
}

this.WifiP2pManager.INTERFACE_NAME = P2P_INTERFACE_NAME;