diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index f3ace569b4f5..5240c7946867 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -403,6 +403,8 @@ #ifdef MOZ_WIDGET_GONK @BINPATH@/components/DOMWifiManager.js @BINPATH@/components/DOMWifiManager.manifest +@BINPATH@/components/DOMWifiP2pManager.js +@BINPATH@/components/DOMWifiP2pManager.manifest @BINPATH@/components/NetworkInterfaceListService.js @BINPATH@/components/NetworkInterfaceListService.manifest @BINPATH@/components/NetworkManager.js diff --git a/dom/system/gonk/nsINetworkManager.idl b/dom/system/gonk/nsINetworkManager.idl index 77c8e4ae902f..acd9c7eadf62 100644 --- a/dom/system/gonk/nsINetworkManager.idl +++ b/dom/system/gonk/nsINetworkManager.idl @@ -9,7 +9,7 @@ interface nsIWifiTetheringCallback; /** * Information about networks that is exposed to network manager API consumers. */ -[scriptable, uuid(f4cf9d88-f962-4d29-9baa-fb295dad387b)] +[scriptable, uuid(e2f5c6e0-4203-11e3-aa6e-0800200c9a66)] interface nsINetworkInterface : nsISupports { const long NETWORK_STATE_UNKNOWN = -1; @@ -31,6 +31,7 @@ interface nsINetworkInterface : nsISupports const long NETWORK_TYPE_MOBILE = 1; const long NETWORK_TYPE_MOBILE_MMS = 2; const long NETWORK_TYPE_MOBILE_SUPL = 3; + const long NETWORK_TYPE_WIFI_P2P = 4; /** * Network type. One of the NETWORK_TYPE_* constants. diff --git a/dom/wifi/StateMachine.jsm b/dom/wifi/StateMachine.jsm new file mode 100644 index 000000000000..ab743a30348e --- /dev/null +++ b/dom/wifi/StateMachine.jsm @@ -0,0 +1,205 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/Services.jsm"); + +this.EXPORTED_SYMBOLS = ["StateMachine"]; + +const DEBUG = false; + +this.StateMachine = function(aDebugTag) { + function debug(aMsg) { + dump('-------------- StateMachine:' + aDebugTag + ': ' + aMsg); + } + + var sm = {}; + + var _initialState; + var _curState; + var _prevState; + var _paused; + var _eventQueue = []; + var _deferredEventQueue = []; + var _defaultEventHandler; + + // Public interfaces. + + sm.setDefaultEventHandler = function(aDefaultEventHandler) { + _defaultEventHandler = aDefaultEventHandler; + }; + + sm.start = function(aInitialState) { + _initialState = aInitialState; + sm.gotoState(_initialState); + }; + + sm.sendEvent = function (aEvent) { + if (!_initialState) { + if (DEBUG) { + debug('StateMachine is not running. Call StateMachine.start() first.'); + } + return; + } + _eventQueue.push(aEvent); + asyncCall(handleFirstEvent); + }; + + sm.getPreviousState = function() { + return _prevState; + }; + + sm.getCurrentState = function() { + return _curState; + }; + + // State object maker. + // @param aName string for this state's name. + // @param aDelegate object: + // .handleEvent: required. + // .enter: called before entering this state (optional). + // .exit: called before exiting this state (optional). + sm.makeState = function (aName, aDelegate) { + if (!aDelegate.handleEvent) { + throw "handleEvent is a required delegate function."; + } + var nop = function() {}; + return { + name: aName, + enter: (aDelegate.enter || nop), + exit: (aDelegate.exit || nop), + handleEvent: aDelegate.handleEvent + }; + }; + + sm.deferEvent = function (aEvent) { + // The definition of a 'deferred event' is: + // We are not able to handle this event now but after receiving + // certain event or entering a new state, we might be able to handle + // it. For example, we couldn't handle CONNECT_EVENT in the + // diconnecting state. But once we finish doing "disconnecting", we + // could then handle CONNECT_EVENT! + // + // So, the deferred event may be handled in the following cases: + // 1. Once we entered a new state. + // 2. Once we handled a regular event. + if (DEBUG) { + debug('Deferring event: ' + JSON.stringify(aEvent)); + } + _deferredEventQueue.push(aEvent); + }; + + // Goto the new state. If the current state is null, the exit + // function won't be called. + sm.gotoState = function (aNewState) { + if (_curState) { + if (DEBUG) { + debug("exiting state: " + _curState.name); + } + _curState.exit(); + } + + _prevState = _curState; + _curState = aNewState; + + if (DEBUG) { + debug("entering state: " + _curState.name); + } + _curState.enter(); + + // We are in the new state now. We got a chance to handle the + // deferred events. + handleDeferredEvents(); + + sm.resume(); + }; + + // No incoming event will be handled after you call pause(). + // (But they will be queued.) + sm.pause = function() { + _paused = true; + }; + + // Continue to handle incoming events. + sm.resume = function() { + _paused = false; + asyncCall(handleFirstEvent); + }; + + //---------------------------------------------------------- + // Private stuff + //---------------------------------------------------------- + + function asyncCall(f) { + Services.tm.currentThread.dispatch(f, Ci.nsIThread.DISPATCH_NORMAL); + } + + function handleFirstEvent() { + var hadDeferredEvents; + + if (0 === _eventQueue.length) { + return; + } + + if (_paused) { + return; // The state machine is paused now. + } + + hadDeferredEvents = _deferredEventQueue.length > 0; + + handleOneEvent(_eventQueue.shift()); // The handler may defer this event. + + // We've handled one event. If we had deferred events before, now is + // a good chance to handle them. + if (hadDeferredEvents) { + handleDeferredEvents(); + } + + // Continue to handle the next regular event. + handleFirstEvent(); + } + + function handleDeferredEvents() { + if (_deferredEventQueue.length && DEBUG) { + debug('Handle deferred events: ' + _deferredEventQueue.length); + } + for (let i = 0; i < _deferredEventQueue.length; i++) { + handleOneEvent(_deferredEventQueue.shift()); + } + } + + function handleOneEvent(aEvent) + { + if (DEBUG) { + debug('Handling event: ' + JSON.stringify(aEvent)); + } + + var handled = _curState.handleEvent(aEvent); + + if (undefined === handled) { + throw "handleEvent returns undefined: " + _curState.name; + } + if (!handled) { + // Event is not handled in the current state. Try handleEventCommon(). + handled = (_defaultEventHandler ? _defaultEventHandler(aEvent) : handled); + } + if (undefined === handled) { + throw "handleEventCommon returns undefined: " + _curState.name; + } + if (!handled) { + if (DEBUG) { + debug('!!!!!!!!! FIXME !!!!!!!!! Event not handled: ' + JSON.stringify(aEvent)); + } + } + + return handled; + } + + return sm; +}; diff --git a/dom/wifi/WifiCommand.jsm b/dom/wifi/WifiCommand.jsm index b7a550566e6a..6456628e1394 100644 --- a/dom/wifi/WifiCommand.jsm +++ b/dom/wifi/WifiCommand.jsm @@ -14,8 +14,15 @@ Cu.import("resource://gre/modules/systemlibs.js"); const SUPP_PROP = "init.svc.wpa_supplicant"; const WPA_SUPPLICANT = "wpa_supplicant"; +const DEBUG = false; this.WifiCommand = function(aControlMessage, aInterface) { + function debug(msg) { + if (DEBUG) { + dump('-------------- WifiCommand: ' + msg); + } + } + var command = {}; //------------------------------------------------- @@ -135,14 +142,16 @@ this.WifiCommand = function(aControlMessage, aInterface) { doStringCommand("LOG_LEVEL", callback); }; - command.wpsPbc = function (callback) { - doBooleanCommand("WPS_PBC", "OK", callback); + command.wpsPbc = function (iface, callback) { + doBooleanCommand("WPS_PBC" + (iface ? (" interface=" + iface) : ""), + "OK", callback); }; command.wpsPin = function (detail, callback) { doStringCommand("WPS_PIN " + (detail.bssid === undefined ? "any" : detail.bssid) + - (detail.pin === undefined ? "" : (" " + detail.pin)), + (detail.pin === undefined ? "" : (" " + detail.pin)) + + (detail.iface ? (" interface=" + detail.iface) : ""), callback); }; @@ -337,9 +346,89 @@ this.WifiCommand = function(aControlMessage, aInterface) { }); }; - //-------------------------------------------------- - // Helper functions. - //-------------------------------------------------- + command.setDeviceName = function(deviceName, callback) { + doBooleanCommand("SET device_name " + deviceName, "OK", callback); + }; + + //------------------------------------------------- + // P2P commands. + //------------------------------------------------- + + command.p2pProvDiscovery = function(address, wpsMethod, callback) { + var command = "P2P_PROV_DISC " + address + " " + wpsMethod; + doBooleanCommand(command, "OK", callback); + }; + + command.p2pConnect = function(config, callback) { + var command = "P2P_CONNECT " + config.address + " " + config.wpsMethodWithPin + " "; + if (config.joinExistingGroup) { + command += "join"; + } else { + command += "go_intent=" + config.goIntent; + } + + debug('P2P connect command: ' + command); + doBooleanCommand(command, "OK", callback); + }; + + command.p2pGroupRemove = function(iface, callback) { + debug("groupRemove()"); + doBooleanCommand("P2P_GROUP_REMOVE " + iface, "OK", callback); + }; + + command.p2pEnable = function(detail, callback) { + var commandChain = ["SET device_name " + detail.deviceName, + "SET device_type " + detail.deviceType, + "SET config_methods " + detail.wpsMethods, + "P2P_SET conc_pref sta", + "P2P_FLUSH"]; + + doBooleanCommandChain(commandChain, callback); + }; + + command.p2pDisable = function(callback) { + doBooleanCommand("P2P_SET disabled 1", "OK", callback); + }; + + command.p2pEnableScan = function(timeout, callback) { + doBooleanCommand("P2P_FIND " + timeout, "OK", callback); + }; + + command.p2pDisableScan = function(callback) { + doBooleanCommand("P2P_STOP_FIND", "OK", callback); + }; + + command.p2pGetGroupCapab = function(address, callback) { + command.p2pPeer(address, function(reply) { + debug('p2p_peer reply: ' + reply); + if (!reply) { + callback(0); + return; + } + var capab = /group_capab=0x([0-9a-fA-F]+)/.exec(reply)[1]; + if (!capab) { + callback(0); + } else { + callback(parseInt(capab, 16)); + } + }); + }; + + command.p2pPeer = function(address, callback) { + doStringCommand("P2P_PEER " + address, callback); + }; + + command.p2pGroupAdd = function(netId, callback) { + doBooleanCommand("P2P_GROUP_ADD persistent=" + netId, callback); + }; + + command.p2pReinvoke = function(netId, address, callback) { + doBooleanCommand("P2P_INVITE persistent=" + netId + " peer=" + address, "OK", callback); + }; + + //---------------------------------------------------------- + // Private stuff. + //---------------------------------------------------------- function voidControlMessage(cmd, callback) { aControlMessage({ cmd: cmd, iface: aInterface }, function (data) { @@ -391,6 +480,10 @@ this.WifiCommand = function(aControlMessage, aInterface) { }); } + //-------------------------------------------------- + // Helper functions. + //-------------------------------------------------- + function stopProcess(service, process, callback) { var count = 0; var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); diff --git a/dom/wifi/WifiNetUtil.jsm b/dom/wifi/WifiNetUtil.jsm index c672183e4ce3..d79542ceda6f 100644 --- a/dom/wifi/WifiNetUtil.jsm +++ b/dom/wifi/WifiNetUtil.jsm @@ -11,16 +11,23 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/systemlibs.js"); -XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", - "@mozilla.org/network/manager;1", - "nsINetworkManager"); +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); this.EXPORTED_SYMBOLS = ["WifiNetUtil"]; const DHCP_PROP = "init.svc.dhcpcd"; const DHCP = "dhcpcd"; +const DEBUG = false; this.WifiNetUtil = function(controlMessage) { + function debug(msg) { + if (DEBUG) { + dump('-------------- NetUtil: ' + msg); + } + } + var util = {}; util.configureInterface = function(cfg, callback) { @@ -67,14 +74,14 @@ this.WifiNetUtil = function(controlMessage) { }); }; - util.startDhcpServer = function (range, callback) { - gNetworkManager.setDhcpServer(true, range, function (error) { + util.startDhcpServer = function (config, callback) { + gNetworkService.setDhcpServer(true, config, function (error) { callback(!error); }); }; util.stopDhcpServer = function (callback) { - gNetworkManager.setDhcpServer(false, null, function (error) { + gNetworkService.setDhcpServer(false, null, function (error) { callback(!error); }); }; @@ -135,6 +142,7 @@ this.WifiNetUtil = function(controlMessage) { util.runIpConfig = function (name, data, callback) { if (!data) { + debug("IP config failed to run"); callback({ info: data }); return; } @@ -142,16 +150,19 @@ this.WifiNetUtil = function(controlMessage) { setProperty("net." + name + ".dns1", ipToString(data.dns1), function(ok) { if (!ok) { + debug("Unable to set net..dns1"); return; } setProperty("net." + name + ".dns2", ipToString(data.dns2), function(ok) { if (!ok) { + debug("Unable to set net..dns2"); return; } setProperty("net." + name + ".gw", ipToString(data.gateway), function(ok) { if (!ok) { + debug("Unable to set net..gw"); return; } callback({ info: data }); diff --git a/dom/wifi/WifiP2pManager.jsm b/dom/wifi/WifiP2pManager.jsm new file mode 100644 index 000000000000..93499cfacb12 --- /dev/null +++ b/dom/wifi/WifiP2pManager.jsm @@ -0,0 +1,1597 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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"); + +const kNetworkInterfaceStateChangedTopic = "network-interface-state-changed"; + +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", + netmask: "255.255.255.0", // Should be consistent with |maskLenth|. + maskLength: 24, // Should be consistent with |netmask|. + broadcast: "192.168.2.255", + 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" +}; + +let 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]), + + state: Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED, + type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI_P2P, + name: P2P_INTERFACE_NAME, + ip: null, + netmask: null, + broadcast: null, + dns1: null, + dns2: null, + gateway: null, + 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); + _sm.gotoState(stateDisabled); + } + + function onSuccess() + { + _onEnabled(true); + _sm.gotoState(stateInactive); + } + + _sm.pause(); + + // Step 1: Connect to p2p0. + aP2pCommand.connectToSupplicant(function (status) { + let detail; + + if (0 !== status) { + debug('Failed to connect to p2p0'); + onFailure(); + return; + } + + debug('wpa_supplicant p2p0 connected!'); + _onSupplicantConnected(); + + // 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. + aNetUtil.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.gateway, + 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(_groupInfo.ifname, onWpsCommandSent); + } 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'); + aP2pCommand.closeSupplicantConnection(function (status) { + debug('Supplicant connection closed'); + aNetUtil.disableInterface(P2P_INTERFACE_NAME, function (success){ + debug('Disabled interface: ' + P2P_INTERFACE_NAME); + _onDisabled(true); + _sm.gotoState(stateDisabled); + }); + }); + }); + }); + }, + + 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.state = Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED; + _p2pNetworkInterface.ip = GO_NETWORK_INTERFACE.ip; + _p2pNetworkInterface.netmask = GO_NETWORK_INTERFACE.netmask; + _p2pNetworkInterface.gateway = 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, 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. + _p2pNetworkInterface.state = Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED; + _p2pNetworkInterface.ip = dhcpData.info.ipaddr_str; + _p2pNetworkInterface.netmask = dhcpData.info.mask_str; + _p2pNetworkInterface.broadcast = dhcpData.info.broadcast_str; + _p2pNetworkInterface.dns1 = dhcpData.info.dns1_str; + _p2pNetworkInterface.dns2 = dhcpData.info.dns2_str; + _p2pNetworkInterface.gateway = dhcpData.info.gateway_str; + handleP2pNetworkInterfaceStateChanged(); + + _groupInfo.networkInterface = _p2pNetworkInterface; + + debug('Happy p2p client~'); + onSuccess(); + }); + } + + function resetP2pNetworkInterface() { + _p2pNetworkInterface.state = Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED; + _p2pNetworkInterface.ip = null; + _p2pNetworkInterface.netmask = null; + _p2pNetworkInterface.broadcast = null; + _p2pNetworkInterface.dns1 = null; + _p2pNetworkInterface.dns2 = null; + _p2pNetworkInterface.gateway = null; + } + + 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() { + Services.obs.notifyObservers(_p2pNetworkInterface, + kNetworkInterfaceStateChangedTopic, + null); + } + + // 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.state = Ci.nsINetworkInterface.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; diff --git a/dom/wifi/WifiP2pWorkerObserver.jsm b/dom/wifi/WifiP2pWorkerObserver.jsm new file mode 100644 index 000000000000..69d0edead4f9 --- /dev/null +++ b/dom/wifi/WifiP2pWorkerObserver.jsm @@ -0,0 +1,303 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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; + +const CONNECTION_STATUS_DISCONNECTED = "disconnected"; +const CONNECTION_STATUS_CONNECTING = "connecting"; +const CONNECTION_STATUS_CONNECTED = "connected"; +const CONNECTION_STATUS_DISCONNECTING = "disconnecting"; + +const DEBUG = false; + +this.EXPORTED_SYMBOLS = ["WifiP2pWorkerObserver"]; + +// WifiP2pWorkerObserver resides in WifiWorker to handle DOM message +// by either 1) returning internally maintained information or +// 2) delegating to aDomMsgResponder. It is also responsible +// for observing events from WifiP2pManager and dispatch to DOM. +// +// @param aDomMsgResponder handles DOM messages, including +// - setScanEnabled +// - connect +// - disconnect +// - setPairingConfirmation +// The instance is actually WifiP2pManager. +this.WifiP2pWorkerObserver = function(aDomMsgResponder) { + function debug(aMsg) { + if (DEBUG) { + dump('-------------- WifiP2pWorkerObserver: ' + aMsg); + } + } + + // Private member variables. + let _localDevice; + let _peerList = {}; // List of P2pDevice. + let _domManagers = []; + + // Constructor of P2pDevice. It will be exposed to DOM. + // + // @param aPeer object representing a P2P device: + // .name: string for the device name. + // .address: Mac address. + // .isGroupOwner: boolean to indicate if this device is the group owner. + // .wpsCapabilities: array of string of {"pbc", "display", "keypad"}. + function P2pDevice(aPeer) { + this.address = aPeer.address; + this.name = (aPeer.name ? aPeer.name : aPeer.address); + this.isGroupOwner = aPeer.isGroupOwner; + this.wpsCapabilities = aPeer.wpsCapabilities; + this.connectionStatus = CONNECTION_STATUS_DISCONNECTED; + + // Since this object will be exposed to web, defined the exposed + // properties here. + this.__exposedProps__ = { + address: "r", + name: "r", + isGroupOwner: "r", + wpsCapabilities: "r", + connectionStatus: "r" + }; + } + + // Constructor of P2pGroupOwner. + // + // @param aGroupOwner: + // .macAddress + // .ipAddress + // .passphrase + // .ssid + // .freq + // .isLocal + function P2pGroupOwner(aGroupOwner) { + this.macAddress = aGroupOwner.macAddress; // The identifier to get further information. + this.ipAddress = aGroupOwner.ipAddress; + this.passphrase = aGroupOwner.passphrase; + this.ssid = aGroupOwner.ssid; // e.g. DIRECT-xy. + this.freq = aGroupOwner.freq; + this.isLocal = aGroupOwner.isLocal; + + let detail = _peerList[aGroupOwner.macAddress]; + if (detail) { + this.name = detail.name; + this.wpsCapabilities = detail.wpsCapabilities; + } else if (_localDevice.address === this.macAddress) { + this.name = _localDevice.name; + this.wpsCapabilities = _localDevice.wpsCapabilities; + } else { + debug("We don't know this group owner: " + aGroupOwner.macAddress); + this.name = aGroupOwner.macAddress; + this.wpsCapabilities = []; + } + } + + function fireEvent(aMessage, aData) { + debug('domManager: ' + JSON.stringify(_domManagers)); + _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("WifiP2pManager:" + aMessage, aData); + }); + } + + function addDomManager(aMsg) { + if (-1 === _domManagers.indexOf(aMsg.manager)) { + _domManagers.push(aMsg.manager); + } + } + + function returnMessage(aMessage, aSuccess, aData, aMsg) { + let rMsg = aMessage + ":Return:" + (aSuccess ? "OK" : "NO"); + aMsg.manager.sendAsyncMessage(rMsg, + { data: aData, rid: aMsg.rid, mid: aMsg.mid }); + } + + function handlePeerListUpdated() { + fireEvent("onpeerinfoupdate", {}); + } + + // Return a literal object as the constructed object. + return { + onLocalDeviceChanged: function(aDevice) { + _localDevice = aDevice; + debug('Local device updated to: ' + JSON.stringify(_localDevice)); + }, + + onEnabled: function() { + _peerList = []; + fireEvent("p2pUp", {}); + }, + + onDisbaled: function() { + fireEvent("p2pDown", {}); + }, + + onPeerFound: function(aPeer) { + let newFoundPeer = new P2pDevice(aPeer); + let origianlPeer = _peerList[aPeer.address]; + _peerList[aPeer.address] = newFoundPeer; + if (origianlPeer) { + newFoundPeer.connectionStatus = origianlPeer.connectionStatus; + } + handlePeerListUpdated(); + }, + + onPeerLost: function(aPeer) { + let lostPeer = _peerList[aPeer.address]; + if (!lostPeer) { + debug('Unknown peer lost: ' + aPeer.address); + return; + } + delete _peerList[aPeer.address]; + handlePeerListUpdated(); + }, + + onConnecting: function(aPeer) { + let peer = _peerList[aPeer.address]; + if (!peer) { + debug('Unknown peer connecting: ' + aPeer.address); + peer = new P2pDevice(aPeer); + _peerList[aPeer.address] = peer; + handlePeerListUpdated(); + } + peer.connectionStatus = CONNECTION_STATUS_CONNECTING; + + fireEvent('onconnecting', { peer: peer }); + }, + + onConnected: function(aGroupOwner, aPeer) { + let go = new P2pGroupOwner(aGroupOwner); + let peer = _peerList[aPeer.address]; + if (!peer) { + debug('Unknown peer connected: ' + aPeer.address); + peer = new P2pDevice(aPeer); + _peerList[aPeer.address] = peer; + handlePeerListUpdated(); + } + peer.connectionStatus = CONNECTION_STATUS_CONNECTED; + peer.isGroupOwner = (aPeer.address === aGroupOwner.address); + + fireEvent('onconnected', { groupOwner: go, peer: peer }); + }, + + onDisconnected: function(aPeer) { + let peer = _peerList[aPeer.address]; + if (!peer) { + debug('Unknown peer disconnected: ' + aPeer.address); + return; + } + + peer.connectionStatus = CONNECTION_STATUS_DISCONNECTED; + fireEvent('ondisconnected', { peer: peer }); + }, + + getObservedDOMMessages: function() { + return [ + "WifiP2pManager:getState", + "WifiP2pManager:getPeerList", + "WifiP2pManager:setScanEnabled", + "WifiP2pManager:connect", + "WifiP2pManager:disconnect", + "WifiP2pManager:setPairingConfirmation", + "WifiP2pManager:setDeviceName" + ]; + }, + + onDOMMessage: function(aMessage) { + let msg = aMessage.data || {}; + msg.manager = aMessage.target; + + if ("child-process-shutdown" === aMessage.name) { + let i; + if (-1 !== (i = _domManagers.indexOf(msg.manager))) { + _domManagers.splice(i, 1); + } + return; + } + + if (!aMessage.target.assertPermission("wifi-manage")) { + return; + } + + switch (aMessage.name) { + case "WifiP2pManager:getState": // A new DOM manager is created. + addDomManager(msg); + return { peerList: _peerList, }; // Synchronous call. Simply return it. + + case "WifiP2pManager:setScanEnabled": + { + let enabled = msg.data; + + aDomMsgResponder.setScanEnabled(enabled, function(success) { + returnMessage(aMessage.name, success, (success ? true : "ERROR"), msg); + }); + } + break; + + case "WifiP2pManager:getPeerList": + { + // Convert the object to an array. + let peerArray = []; + for (let key in _peerList) { + if (_peerList.hasOwnProperty(key)) { + peerArray.push(_peerList[key]); + } + } + + returnMessage(aMessage.name, true, peerArray, msg); + } + break; + + case "WifiP2pManager:connect": + { + let peer = msg.data; + + let onDoConnect = function(success) { + returnMessage(aMessage.name, success, (success ? true : "ERROR"), msg); + }; + + aDomMsgResponder.connect(peer.address, peer.wpsMethod, + peer.goIntent, onDoConnect); + } + break; + + case "WifiP2pManager:disconnect": + { + let address = msg.data; + + aDomMsgResponder.disconnect(address, function(success) { + returnMessage(aMessage.name, success, (success ? true : "ERROR"), msg); + }); + } + break; + + case "WifiP2pManager:setPairingConfirmation": + { + let result = msg.data; + aDomMsgResponder.setPairingConfirmation(result); + returnMessage(aMessage.name, true, true, msg); + } + break; + + case "WifiP2pManager:setDeviceName": + { + let newDeviceName = msg.data; + aDomMsgResponder.setDeviceName(newDeviceName, function(success) { + returnMessage(aMessage.name, success, (success ? true : "ERROR"), msg); + }); + } + break; + + default: + if (0 === aMessage.name.indexOf("WifiP2pManager:")) { + debug("DOM WifiP2pManager message not handled: " + aMessage.name); + } + } // End of switch. + } + }; +}; diff --git a/dom/wifi/WifiWorker.js b/dom/wifi/WifiWorker.js index 57e58a20584e..824e18ddf039 100644 --- a/dom/wifi/WifiWorker.js +++ b/dom/wifi/WifiWorker.js @@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/systemlibs.js"); 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. @@ -108,16 +110,30 @@ var WifiManager = (function() { 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", ifname: libcutils.property_get("wifi.interface") }; } - let {sdkVersion, unloadDriverEnabled, schedScanRecovery, driverDelay, ifname} = getStartupPrefs(); + let {sdkVersion, unloadDriverEnabled, schedScanRecovery, driverDelay, p2pSupported, ifname} = getStartupPrefs(); let wifiListener = { onWaitEvent: function(event, iface) { if (manager.ifname === iface && handleEvent(event)) { waitForEvent(iface); + } else if (p2pSupported) { + if (WifiP2pManager.INTERFACE_NAME === iface) { + // 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); + } } }, @@ -135,18 +151,29 @@ var WifiManager = (function() { 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); + + // Wifi P2P stuff + var p2pManager; + if (p2pSupported) { + let p2pCommand = WifiCommand(controlMessage, WifiP2pManager.INTERFACE_NAME); + 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!"); } - var wifiCommand = WifiCommand(controlMessage, manager.ifname); - var netUtil = WifiNetUtil(controlMessage); - // Callbacks to invoke when a reply arrives from the wifi service. var controlCallbacks = Object.create(null); var idgen = 0; @@ -244,6 +271,7 @@ var WifiManager = (function() { 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 @@ -255,6 +283,7 @@ var WifiManager = (function() { }); return; } + manager.handlePreWifiScan(); wifiCommand.scan(callback); } @@ -267,6 +296,7 @@ var WifiManager = (function() { if (ok) debugEnabled = wanted; }); + p2pManager.setDebug(DEBUG); } } @@ -755,6 +785,7 @@ var WifiManager = (function() { reEnableBackgroundScan = false; setBackgroundScan("ON", function() {}); } + manager.handlePostWifiScan(); notify("scanresultsavailable"); return true; } @@ -786,6 +817,10 @@ var WifiManager = (function() { notify("supplicantconnection"); callback(); }); + + if (p2pSupported) { + manager.enableP2p(function(success) {}); + } } function prepareForStartup(callback) { @@ -911,19 +946,27 @@ var WifiManager = (function() { // 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. - manager.state = "DISABLING"; - wifiCommand.terminateSupplicant(function (ok) { - manager.connectionDropped(function () { - wifiCommand.stopSupplicant(function (status) { - wifiCommand.closeSupplicantConnection(function () { - manager.state = "UNINITIALIZED"; - netUtil.disableInterface(manager.ifname, function (ok) { - unloadDriver(WIFI_FIRMWARE_STATION, callback); + let doDisableWifi = function() { + manager.state = "DISABLING"; + wifiCommand.terminateSupplicant(function (ok) { + manager.connectionDropped(function () { + wifiCommand.stopSupplicant(function (status) { + wifiCommand.closeSupplicantConnection(function () { + manager.state = "UNINITIALIZED"; + netUtil.disableInterface(manager.ifname, function (ok) { + unloadDriver(WIFI_FIRMWARE_STATION, callback); + }); }); }); }); }); - }); + } + + if (p2pSupported) { + p2pManager.setEnabled(false, { onDisabled: doDisableWifi }); + } else { + doDisableWifi(); + } } } @@ -1135,6 +1178,11 @@ var WifiManager = (function() { 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) { @@ -1215,6 +1263,46 @@ var WifiManager = (function() { } } + 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() { + wifiService.waitForEvent(WifiP2pManager.INTERFACE_NAME); + }, + + onEnabled: function(success) { + callback(success); + } + }); + }; + return manager; })(); @@ -1497,6 +1585,17 @@ function WifiWorker() { 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; @@ -1529,6 +1628,7 @@ function WifiWorker() { // 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; } @@ -2304,6 +2404,15 @@ WifiWorker.prototype = { 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. @@ -2393,79 +2502,6 @@ WifiWorker.prototype = { }).bind(this)); }, - getWifiScanResults: function(callback) { - var count = 0; - var timer = null; - var self = this; - - 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; - this.wantScanResults.splice(this.wantScanResults.indexOf(waitForScanCallback), 1); - callback.onfailure(); - return; - } - - // Else it's still running, continue waiting. - timer.initWithCallback(doScan, 10000, Ci.nsITimer.TYPE_ONE_SHOT); - return; - } - }); - } - - 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) { @@ -2722,7 +2758,7 @@ WifiWorker.prototype = { let self = this; let detail = msg.data; if (detail.method === "pbc") { - WifiManager.wpsPbc(function(ok) { + WifiManager.wpsPbc(WifiManager.ifname, function(ok) { if (ok) self._sendMessage(message, true, true, msg); else diff --git a/dom/wifi/moz.build b/dom/wifi/moz.build index c6c8a54e9c74..8f8f3dd116b9 100644 --- a/dom/wifi/moz.build +++ b/dom/wifi/moz.build @@ -6,6 +6,7 @@ XPIDL_SOURCES += [ 'nsIDOMMozWifiConnectionInfoEvent.idl', + 'nsIDOMMozWifiP2pStatusChangeEvent.idl', 'nsIDOMMozWifiStatusChangeEvent.idl', 'nsIWifi.idl', 'nsIWifiService.idl', @@ -16,13 +17,18 @@ XPIDL_MODULE = 'dom_wifi' EXTRA_COMPONENTS += [ 'DOMWifiManager.js', 'DOMWifiManager.manifest', + 'DOMWifiP2pManager.js', + 'DOMWifiP2pManager.manifest', 'WifiWorker.js', 'WifiWorker.manifest', ] EXTRA_JS_MODULES += [ + 'StateMachine.jsm', 'WifiCommand.jsm', 'WifiNetUtil.jsm', + 'WifiP2pManager.jsm', + 'WifiP2pWorkerObserver.jsm', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':