diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index faa86541ceb0..5e9d3924c1e7 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -307,12 +307,10 @@ pref("image.onload.decode.limit", 24); /* don't decode more than 24 images eager // XXX this isn't a good check for "are touch events supported", but // we don't really have a better one at the moment. -#ifdef MOZ_WIDGET_GONK // enable touch events interfaces pref("dom.w3c_touch_events.enabled", 1); pref("dom.w3c_touch_events.safetyX", 0); // escape borders in units of 1/240" pref("dom.w3c_touch_events.safetyY", 120); // escape borders in units of 1/240" -#endif #ifdef MOZ_SAFE_BROWSING // Safe browsing does nothing unless this pref is set diff --git a/b2g/chrome/content/desktop.js b/b2g/chrome/content/desktop.js new file mode 100644 index 000000000000..311d2e886c8b --- /dev/null +++ b/b2g/chrome/content/desktop.js @@ -0,0 +1,10 @@ + +window.addEventListener("ContentStart", function(evt) { + // Enable touch event shim on desktop that translates mouse events + // into touch ones + let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}) + .devtools.require; + let { TouchEventHandler } = require("devtools/touch-events"); + let touchEventHandler = new TouchEventHandler(shell.contentBrowser); + touchEventHandler.start(); +}); diff --git a/b2g/chrome/content/shell.html b/b2g/chrome/content/shell.html index 8bab69a3bd7b..0cdae8285d9f 100644 --- a/b2g/chrome/content/shell.html +++ b/b2g/chrome/content/shell.html @@ -19,7 +19,9 @@ src="chrome://browser/content/shell.js"> #ifndef MOZ_WIDGET_GONK - + + diff --git a/b2g/chrome/jar.mn b/b2g/chrome/jar.mn index 632668d3f085..4da1121275e3 100644 --- a/b2g/chrome/jar.mn +++ b/b2g/chrome/jar.mn @@ -14,6 +14,7 @@ chrome.jar: * content/shell.html (content/shell.html) * content/shell.js (content/shell.js) #ifndef ANDROID + content/desktop.js (content/desktop.js) content/screen.js (content/screen.js) content/runapp.js (content/runapp.js) #endif diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 73b4dca10f21..bd36f1f9db7e 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "25a3b96e9c5ff89b69b29007462bfd056ad5bf53", + "revision": "ae5c954ca1b8047cfa932f905d6498b47bb44ac5", "repo_path": "/integration/gaia-central" } diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index 23243a990137..f0ea2d6473d3 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -402,6 +402,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/browser/devtools/responsivedesign/test/browser_responsiveui_touch.js b/browser/devtools/responsivedesign/test/browser_responsiveui_touch.js index 9c461d1aabb8..4e990d048e37 100644 --- a/browser/devtools/responsivedesign/test/browser_responsiveui_touch.js +++ b/browser/devtools/responsivedesign/test/browser_responsiveui_touch.js @@ -25,11 +25,11 @@ function test() { function testWithNoTouch() { let div = content.document.querySelector("div"); let x = 2, y = 2; - EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousedown", isSynthesized: false}, content); x += 20; y += 10; - EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousemove", isSynthesized: false}, content); is(div.style.transform, "", "touch didn't work"); - EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mouseup", isSynthesized: false}, content); testWithTouch(); } @@ -37,11 +37,11 @@ function test() { gBrowser.selectedTab.__responsiveUI.enableTouch(); let div = content.document.querySelector("div"); let x = 2, y = 2; - EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousedown", isSynthesized: false}, content); x += 20; y += 10; - EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousemove", isSynthesized: false}, content); is(div.style.transform, "translate(20px, 10px)", "touch worked"); - EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mouseup", isSynthesized: false}, content); is(div.style.transform, "none", "end event worked"); mgr.toggle(window, gBrowser.selectedTab); } @@ -50,11 +50,11 @@ function test() { gBrowser.selectedTab.__responsiveUI.disableTouch(); let div = content.document.querySelector("div"); let x = 2, y = 2; - EventUtils.synthesizeMouse(div, x, y, {type: "mousedown"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousedown", isSynthesized: false}, content); x += 20; y += 10; - EventUtils.synthesizeMouse(div, x, y, {type: "mousemove"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mousemove", isSynthesized: false}, content); is(div.style.transform, "", "touch didn't work"); - EventUtils.synthesizeMouse(div, x, y, {type: "mouseup"}, content); + EventUtils.synthesizeMouse(div, x, y, {type: "mouseup", isSynthesized: false}, content); finishUp(); } diff --git a/content/base/test/test_XHR_system.html b/content/base/test/test_XHR_system.html index 4bdc4fe82a63..31fef6d64b22 100644 --- a/content/base/test/test_XHR_system.html +++ b/content/base/test/test_XHR_system.html @@ -82,6 +82,7 @@ tests.push(function test_redirect_to_file_uri() { function runNextTest() { if (!tests.length) { + SimpleTest.finish(); return; } tests.shift()(); @@ -89,14 +90,8 @@ function runNextTest() { function runTests() { SimpleTest.waitForExplicitFinish(); - SpecialPowers.addPermission("systemXHR", true, document); - tests.push(function tearDown() { - SpecialPowers.removePermission("systemXHR", document); - SimpleTest.finish(); - }); - - runNextTest(); + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runNextTest); } diff --git a/content/media/encoder/MediaEncoder.cpp b/content/media/encoder/MediaEncoder.cpp index a04c992cfde6..ea50fc20413c 100644 --- a/content/media/encoder/MediaEncoder.cpp +++ b/content/media/encoder/MediaEncoder.cpp @@ -7,6 +7,7 @@ #include "nsIPrincipal.h" #include "nsMimeTypes.h" #include "prlog.h" +#include "mozilla/Preferences.h" #ifdef MOZ_OGG #include "OggWriter.h" @@ -92,7 +93,7 @@ MediaEncoder::CreateEncoder(const nsAString& aMIMEType, uint8_t aTrackTypes) return nullptr; } #ifdef MOZ_WEBM_ENCODER - else if (MediaDecoder::IsWebMEnabled() && + else if (MediaEncoder::IsWebMEncoderEnabled() && (aMIMEType.EqualsLiteral(VIDEO_WEBM) || (aTrackTypes & ContainerWriter::HAS_VIDEO))) { if (aTrackTypes & ContainerWriter::HAS_AUDIO) { @@ -107,8 +108,9 @@ MediaEncoder::CreateEncoder(const nsAString& aMIMEType, uint8_t aTrackTypes) } #endif //MOZ_WEBM_ENCODER #ifdef MOZ_OMX_ENCODER - else if (aMIMEType.EqualsLiteral(VIDEO_MP4) || - (aTrackTypes & ContainerWriter::HAS_VIDEO)) { + else if (MediaEncoder::IsOMXEncoderEnabled() && + (aMIMEType.EqualsLiteral(VIDEO_MP4) || + (aTrackTypes & ContainerWriter::HAS_VIDEO))) { if (aTrackTypes & ContainerWriter::HAS_AUDIO) { audioEncoder = new OmxAudioTrackEncoder(); NS_ENSURE_TRUE(audioEncoder, nullptr); @@ -296,4 +298,20 @@ MediaEncoder::CopyMetadataToMuxer(TrackEncoder *aTrackEncoder) return rv; } +#ifdef MOZ_WEBM_ENCODER +bool +MediaEncoder::IsWebMEncoderEnabled() +{ + return Preferences::GetBool("media.encoder.webm.enabled"); +} +#endif + +#ifdef MOZ_OMX_ENCODER +bool +MediaEncoder::IsOMXEncoderEnabled() +{ + return Preferences::GetBool("media.encoder.omx.enabled"); +} +#endif + } diff --git a/content/media/encoder/MediaEncoder.h b/content/media/encoder/MediaEncoder.h index 933363b643eb..f4e1d3087905 100644 --- a/content/media/encoder/MediaEncoder.h +++ b/content/media/encoder/MediaEncoder.h @@ -128,6 +128,14 @@ public : return mState == ENCODE_ERROR; } +#ifdef MOZ_WEBM_ENCODER + static bool IsWebMEncoderEnabled(); +#endif + +#ifdef MOZ_OMX_ENCODER + static bool IsOMXEncoderEnabled(); +#endif + private: // Get encoded data from trackEncoder and write to muxer nsresult WriteEncodedDataToMuxer(TrackEncoder *aTrackEncoder); diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index ce97ad23ae6a..e62a5d05eff4 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -1778,6 +1778,26 @@ Navigator::HasIccManagerSupport(JSContext* /* unused */, nsCOMPtr win = GetWindowFromGlobal(aGlobal); return win && CheckPermission(win, "mobileconnection"); } + +/* static */ +bool +Navigator::HasWifiManagerSupport(JSContext* /* unused */, + JSObject* aGlobal) +{ + // On XBL scope, the global object is NOT |window|. So we have + // to use nsContentUtils::GetObjectPrincipal to get the principal + // and test directly with permission manager. + + nsIPrincipal* principal = nsContentUtils::GetObjectPrincipal(aGlobal); + + nsCOMPtr permMgr = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID); + NS_ENSURE_TRUE(permMgr, false); + + uint32_t permission = nsIPermissionManager::DENY_ACTION; + permMgr->TestPermissionFromPrincipal(principal, "wifi-manage", &permission); + return nsIPermissionManager::ALLOW_ACTION == permission; +} #endif // MOZ_B2G_RIL #ifdef MOZ_B2G_BT diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index 534d3c615be1..aa9327145167 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -264,6 +264,8 @@ public: JSObject* aGlobal); static bool HasIccManagerSupport(JSContext* /* unused */, JSObject* aGlobal); + static bool HasWifiManagerSupport(JSContext* /* unused */, + JSObject* aGlobal); #endif // MOZ_B2G_RIL #ifdef MOZ_B2G_BT static bool HasBluetoothSupport(JSContext* /* unused */, JSObject* aGlobal); diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index 25c8b0db5d3a..a8c095ec9413 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -604,11 +604,14 @@ nsDOMWindowUtils::SendMouseEvent(const nsAString& aType, bool aIgnoreRootScrollFrame, float aPressure, unsigned short aInputSourceArg, + bool aIsSynthesized, + uint8_t aOptionalArgCount, bool *aPreventDefault) { return SendMouseEventCommon(aType, aX, aY, aButton, aClickCount, aModifiers, aIgnoreRootScrollFrame, aPressure, - aInputSourceArg, false, aPreventDefault); + aInputSourceArg, false, aPreventDefault, + aOptionalArgCount >= 4 ? aIsSynthesized : true); } NS_IMETHODIMP @@ -620,12 +623,15 @@ nsDOMWindowUtils::SendMouseEventToWindow(const nsAString& aType, int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure, - unsigned short aInputSourceArg) + unsigned short aInputSourceArg, + bool aIsSynthesized, + uint8_t aOptionalArgCount) { PROFILER_LABEL("nsDOMWindowUtils", "SendMouseEventToWindow"); return SendMouseEventCommon(aType, aX, aY, aButton, aClickCount, aModifiers, aIgnoreRootScrollFrame, aPressure, - aInputSourceArg, true, nullptr); + aInputSourceArg, true, nullptr, + aOptionalArgCount >= 4 ? aIsSynthesized : true); } static LayoutDeviceIntPoint @@ -668,7 +674,8 @@ nsDOMWindowUtils::SendMouseEventCommon(const nsAString& aType, float aPressure, unsigned short aInputSourceArg, bool aToWindow, - bool *aPreventDefault) + bool *aPreventDefault, + bool aIsSynthesized) { if (!nsContentUtils::IsCallerChrome()) { return NS_ERROR_DOM_SECURITY_ERR; @@ -715,7 +722,7 @@ nsDOMWindowUtils::SendMouseEventCommon(const nsAString& aType, event.inputSource = aInputSourceArg; event.clickCount = aClickCount; event.time = PR_IntervalNow(); - event.mFlags.mIsSynthesizedForTests = true; + event.mFlags.mIsSynthesizedForTests = aIsSynthesized; nsPresContext* presContext = GetPresContext(); if (!presContext) diff --git a/dom/base/nsDOMWindowUtils.h b/dom/base/nsDOMWindowUtils.h index 512b0837ef02..21ddf7ee3008 100644 --- a/dom/base/nsDOMWindowUtils.h +++ b/dom/base/nsDOMWindowUtils.h @@ -46,7 +46,8 @@ protected: float aPressure, unsigned short aInputSourceArg, bool aToWindow, - bool *aPreventDefault); + bool *aPreventDefault, + bool aIsSynthesized); NS_IMETHOD SendTouchEventCommon(const nsAString& aType, uint32_t* aIdentifiers, diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index b8b227fe8e75..2bbe15678868 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -49,3 +49,4 @@ support-files = [test_window_extensible.html] [test_window_indexing.html] [test_writable-replaceable.html] +[test_domwindowutils.html] diff --git a/dom/base/test/test_domwindowutils.html b/dom/base/test/test_domwindowutils.html new file mode 100644 index 000000000000..818144ebc32f --- /dev/null +++ b/dom/base/test/test_domwindowutils.html @@ -0,0 +1,84 @@ + + + + + Test for DOMWindowUtils + + + + + +
+
+
+ + diff --git a/dom/events/nsDOMEvent.h b/dom/events/nsDOMEvent.h index fbcd9341d498..5f4993b21cbc 100644 --- a/dom/events/nsDOMEvent.h +++ b/dom/events/nsDOMEvent.h @@ -179,6 +179,11 @@ public: return mEvent->mFlags.mIsTrusted; } + bool IsSynthesized() const + { + return mEvent->mFlags.mIsSynthesizedForTests; + } + uint64_t TimeStamp() const { return mEvent->time; diff --git a/dom/inputmethod/mochitest/test_basic.html b/dom/inputmethod/mochitest/test_basic.html index 8dd3091a76fa..75cbdbbebdf0 100644 --- a/dom/inputmethod/mochitest/test_basic.html +++ b/dom/inputmethod/mochitest/test_basic.html @@ -118,11 +118,8 @@ function test_setComposition() { function test_endComposition() { gContext.endComposition('2013').then(function() { - if (gContext.textBeforeCursor + gContext.textAfterCursor == 'Xulei2013') { - ok(true, 'endComposition changed the input field correctly.'); - } else { - todo(false, 'endComposition changed the input field incorrectly.'); - } + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei2013', + 'endComposition changed the input field correctly.'); test_onSelectionChange(); }, function (e) { ok(false, 'endComposition failed: ' + e.name); diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index e6b8180114f6..22c1452ddfcd 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -43,7 +43,7 @@ interface nsIDOMEventTarget; interface nsIRunnable; interface nsICompositionStringSynthesizer; -[scriptable, uuid(c6efd629-7282-4f0d-9db8-0fa59c191dd5)] +[scriptable, uuid(fa0fe174-7c07-11e3-a5ba-000c290c393e)] interface nsIDOMWindowUtils : nsISupports { /** @@ -249,9 +249,13 @@ interface nsIDOMWindowUtils : nsISupports { * @param aPressure touch input pressure: 0.0 -> 1.0 * @param aInputSourceArg input source, see nsIDOMMouseEvent for values, * defaults to mouse input. + * @param aIsSynthesized controls nsIDOMEvent.isSynthesized value + * that helps identifying test related events, + * defaults to true * * returns true if the page called prevent default on this event */ + [optional_argc] boolean sendMouseEvent(in AString aType, in float aX, in float aY, @@ -260,7 +264,8 @@ interface nsIDOMWindowUtils : nsISupports { in long aModifiers, [optional] in boolean aIgnoreRootScrollFrame, [optional] in float aPressure, - [optional] in unsigned short aInputSourceArg); + [optional] in unsigned short aInputSourceArg, + [optional] in boolean aIsSynthesized); /** Synthesize a touch event. The event types supported are: * touchstart, touchend, touchmove, and touchcancel @@ -303,6 +308,7 @@ interface nsIDOMWindowUtils : nsISupports { /** The same as sendMouseEvent but ensures that the event is dispatched to * this DOM window or one of its children. */ + [optional_argc] void sendMouseEventToWindow(in AString aType, in float aX, in float aY, @@ -311,7 +317,8 @@ interface nsIDOMWindowUtils : nsISupports { in long aModifiers, [optional] in boolean aIgnoreRootScrollFrame, [optional] in float aPressure, - [optional] in unsigned short aInputSourceArg); + [optional] in unsigned short aInputSourceArg, + [optional] in boolean aIsSynthesized); /** The same as sendTouchEvent but ensures that the event is dispatched to * this DOM window or one of its children. diff --git a/dom/ipc/TabChild.cpp b/dom/ipc/TabChild.cpp index b08d0a5814cc..40af3079f3d5 100644 --- a/dom/ipc/TabChild.cpp +++ b/dom/ipc/TabChild.cpp @@ -2387,7 +2387,7 @@ TabChild::DispatchMouseEvent(const nsString& aType, bool defaultPrevented = false; utils->SendMouseEvent(aType, aPoint.x, aPoint.y, aButton, aClickCount, aModifiers, - aIgnoreRootScrollFrame, 0, aInputSourceArg, &defaultPrevented); + aIgnoreRootScrollFrame, 0, aInputSourceArg, false, 4, &defaultPrevented); return defaultPrevented; } diff --git a/dom/ipc/TabParent.cpp b/dom/ipc/TabParent.cpp index 2ba6c16109ac..16dcea38413b 100644 --- a/dom/ipc/TabParent.cpp +++ b/dom/ipc/TabParent.cpp @@ -1116,7 +1116,7 @@ TabParent::SendCompositionEvent(WidgetCompositionEvent& event) mIMECompositionStart = std::min(mIMESelectionAnchor, mIMESelectionFocus); if (mIMECompositionEnding) return true; - event.seqno = ++mIMESeqno; + event.mSeqno = ++mIMESeqno; return PBrowserParent::SendCompositionEvent(event); } @@ -1147,7 +1147,7 @@ TabParent::SendTextEvent(WidgetTextEvent& event) mIMESelectionAnchor = mIMESelectionFocus = mIMECompositionStart + event.theText.Length(); - event.seqno = ++mIMESeqno; + event.mSeqno = ++mIMESeqno; return PBrowserParent::SendTextEvent(event); } @@ -1159,7 +1159,7 @@ TabParent::SendSelectionEvent(WidgetSelectionEvent& event) } mIMESelectionAnchor = event.mOffset + (event.mReversed ? event.mLength : 0); mIMESelectionFocus = event.mOffset + (!event.mReversed ? event.mLength : 0); - event.seqno = ++mIMESeqno; + event.mSeqno = ++mIMESeqno; return PBrowserParent::SendSelectionEvent(event); } diff --git a/dom/messages/SystemMessagePermissionsChecker.jsm b/dom/messages/SystemMessagePermissionsChecker.jsm index e4de5ab0ecf3..74e550e17ae1 100644 --- a/dom/messages/SystemMessagePermissionsChecker.jsm +++ b/dom/messages/SystemMessagePermissionsChecker.jsm @@ -113,6 +113,7 @@ this.SystemMessagePermissionsTable = { "nfc-powerlevel-change": { "settings": ["read", "write"] }, + "wifip2p-pairing-request": { }, }; this.SystemMessagePermissionsChecker = { diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js index 362010e387aa..4bd47353c5b8 100644 --- a/dom/system/gonk/RadioInterfaceLayer.js +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -46,6 +46,11 @@ function debug(s) { let RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") == "true"; +// Ril quirk to always turn the radio off for the client without SIM card +// except hw default client. +let RILQUIRKS_RADIO_OFF_WO_CARD = + libcutils.property_get("ro.moz.ril.radio_off_wo_card", "false") == "true"; + const RADIOINTERFACELAYER_CID = Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}"); const RADIOINTERFACE_CID = @@ -90,6 +95,7 @@ const DOM_MOBILE_MESSAGE_DELIVERY_ERROR = "error"; const RADIO_POWER_OFF_TIMEOUT = 30000; const SMS_HANDLED_WAKELOCK_TIMEOUT = 5000; +const HW_DEFAULT_CLIENT_ID = 0; const RIL_IPC_MOBILECONNECTION_MSG_NAMES = [ "RIL:GetRilContext", @@ -502,6 +508,8 @@ XPCOMUtils.defineLazyGetter(this, "gMessageManager", function() { XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + ril: null, pendingMessages: [], // For queueing "RIL:SetRadioEnabled" messages. timer: null, @@ -510,6 +518,7 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { init: function(ril) { this.ril = ril; + Services.obs.addObserver(this, kSysMsgListenerReadyObserverTopic, false); }, receiveMessage: function(msg) { @@ -541,9 +550,47 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { this._handleMessage(msg); }, + _getNumCards: function() { + let numCards = 0; + for (let i = 0, N = this.ril.numRadioInterfaces; i < N; ++i) { + if (this._isCardPresentAtClient(i)) { + numCards++; + } + } + return numCards; + }, + + _isCardPresentAtClient: function(clientId) { + let cardState = this.ril.getRadioInterface(clientId).rilContext.cardState; + return cardState !== RIL.GECKO_CARDSTATE_UNDETECTED && + cardState !== RIL.GECKO_CARDSTATE_UNKNOWN; + }, + + _isRadioAbleToEnableAtClient: function(clientId, numCards) { + if (!RILQUIRKS_RADIO_OFF_WO_CARD) { + return true; + } + + // We could only turn on the radio for clientId if + // 1. a SIM card is presented or + // 2. it is the default clientId and there is no any SIM card at any client. + + if (this._isCardPresentAtClient(clientId)) { + return true; + } + + numCards = numCards == null ? this._getNumCards() : numCards; + if (clientId === HW_DEFAULT_CLIENT_ID && numCards === 0) { + return true; + } + + return false; + }, + _handleMessage: function(msg) { if (DEBUG) debug("setRadioEnabled: handleMessage: " + JSON.stringify(msg)); - let radioInterface = this.ril.getRadioInterface(msg.json.clientId || 0); + let clientId = msg.json.clientId || 0; + let radioInterface = this.ril.getRadioInterface(clientId); if (!radioInterface.isValidStateForSetRadioEnabled()) { radioInterface.setRadioEnabledResponse(msg.target, msg.json.data, @@ -559,7 +606,13 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { } if (msg.json.data.enabled) { - radioInterface.receiveMessage(msg); + if (this._isRadioAbleToEnableAtClient(clientId)) { + radioInterface.receiveMessage(msg); + } else { + // Not really do it but respond success. + radioInterface.setRadioEnabledResponse(msg.target, msg.json.data); + } + this._processNextMessage(); } else { this.request = (function() { @@ -620,6 +673,27 @@ XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { this.request = null; } this._processNextMessage(); + }, + + /** + * nsIObserver interface methods. + */ + observe: function observe(subject, topic, data) { + switch (topic) { + case kSysMsgListenerReadyObserverTopic: + Services.obs.removeObserver(this, kSysMsgListenerReadyObserverTopic); + + let numCards = this._getNumCards(); + for (let i = 0, N = this.ril.numRadioInterfaces; i < N; ++i) { + if (this._isRadioAbleToEnableAtClient(i, numCards)) { + let radioInterface = this.ril.getRadioInterface(i); + radioInterface.setRadioEnabledInternal({enabled: true}, null); + } + } + break; + default: + break; + } } }; }); @@ -1181,7 +1255,6 @@ function RadioInterface(options) { Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); - Services.obs.addObserver(this, kSysMsgListenerReadyObserverTopic, false); Services.obs.addObserver(this, kSysClockChangeObserverTopic, false); Services.obs.addObserver(this, kScreenStateChangedTopic, false); @@ -2659,9 +2732,6 @@ RadioInterface.prototype = { observe: function(subject, topic, data) { switch (topic) { - case kSysMsgListenerReadyObserverTopic: - this.setRadioEnabledInternal({enabled: true}, null); - break; case kMozSettingsChangedObserverTopic: let setting = JSON.parse(data); this.handleSettingsChange(setting.key, setting.value, setting.message); @@ -3248,6 +3318,15 @@ RadioInterface.prototype = { _fragmentText7Bit: function(text, langTable, langShiftTable, segmentSeptets, strict7BitEncoding) { let ret = []; let body = "", len = 0; + // If the message is empty, we only push the empty message to ret. + if (text.length == 0) { + ret.push({ + body: text, + encodedBodyLength: text.length, + }); + return ret; + } + for (let i = 0, inc = 0; i < text.length; i++) { let c = text.charAt(i); if (strict7BitEncoding) { @@ -3317,6 +3396,15 @@ RadioInterface.prototype = { */ _fragmentTextUCS2: function(text, segmentChars) { let ret = []; + // If the message is empty, we only push the empty message to ret. + if (text.length == 0) { + ret.push({ + body: text, + encodedBodyLength: text.length, + }); + return ret; + } + for (let offset = 0; offset < text.length; offset += segmentChars) { let str = text.substr(offset, segmentChars); ret.push({ 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/telephony/Telephony.cpp b/dom/telephony/Telephony.cpp index 0de1b3eb6518..cbde08f20aff 100644 --- a/dom/telephony/Telephony.cpp +++ b/dom/telephony/Telephony.cpp @@ -697,11 +697,11 @@ Telephony::NotifyError(uint32_t aServiceId, NS_IMETHODIMP Telephony::NotifyCdmaCallWaiting(uint32_t aServiceId, const nsAString& aNumber) { - MOZ_ASSERT(mActiveCall && - mActiveCall->ServiceId() == aServiceId && - mActiveCall->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED); + MOZ_ASSERT(mCalls.Length() == 1); + + nsRefPtr callToNotify = mCalls[0]; + MOZ_ASSERT(callToNotify && callToNotify->ServiceId() == aServiceId); - nsRefPtr callToNotify = mActiveCall; callToNotify->UpdateSecondNumber(aNumber); DispatchCallEvent(NS_LITERAL_STRING("callschanged"), callToNotify); return NS_OK; diff --git a/dom/tests/mochitest/general/test_interfaces.html b/dom/tests/mochitest/general/test_interfaces.html index 3603136ccfb8..e0bdda4a5883 100644 --- a/dom/tests/mochitest/general/test_interfaces.html +++ b/dom/tests/mochitest/general/test_interfaces.html @@ -692,6 +692,12 @@ var interfaceNamesInGlobalScope = "MozWakeLock", // IMPORTANT: Do not change this list without review from a DOM peer! {name: "MozWifiConnectionInfoEvent", b2g: true}, +// IMPORTANT: Do not change this list without review from a DOM peer! + {name: "MozWifiP2pGroupOwner", b2g: true}, +// IMPORTANT: Do not change this list without review from a DOM peer! + {name: "MozWifiP2pManager", b2g: true}, +// IMPORTANT: Do not change this list without review from a DOM peer! + {name: "MozWifiP2pStatusChangeEvent", b2g: true}, // IMPORTANT: Do not change this list without review from a DOM peer! {name: "MozWifiStatusChangeEvent", b2g: true}, // IMPORTANT: Do not change this list without review from a DOM peer! diff --git a/dom/webidl/Event.webidl b/dom/webidl/Event.webidl index 164415896d13..bc5f1f0e5fb6 100644 --- a/dom/webidl/Event.webidl +++ b/dom/webidl/Event.webidl @@ -56,6 +56,7 @@ partial interface Event { readonly attribute EventTarget? originalTarget; readonly attribute EventTarget? explicitOriginalTarget; [ChromeOnly] readonly attribute boolean multipleActionsPrevented; + [ChromeOnly] readonly attribute boolean isSynthesized; boolean getPreventDefault(); }; diff --git a/dom/webidl/MozWifiP2pManager.webidl b/dom/webidl/MozWifiP2pManager.webidl new file mode 100644 index 000000000000..400fa1fd80c1 --- /dev/null +++ b/dom/webidl/MozWifiP2pManager.webidl @@ -0,0 +1,146 @@ +/* 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/. */ + +enum WPSMethod { + "pbc", + "keypad", + "display" +}; + +dictionary WPSInfo { + WPSMethod method; + DOMString pin; +}; + +[JSImplementation="@mozilla.org/wifip2pgroupowner;1"] +interface MozWifiP2pGroupOwner { + readonly attribute DOMString groupName; + readonly attribute DOMString macAddress; + readonly attribute DOMString ipAddress; + readonly attribute DOMString passphrase; + readonly attribute DOMString ssid; + readonly attribute any wpsCapabilities; + readonly attribute unsigned long freq; + readonly attribute boolean isLocal; +}; + +[JSImplementation="@mozilla.org/wifip2pmanager;1", + NavigatorProperty="mozWifiP2pManager", + Func="Navigator::HasWifiManagerSupport"] +interface MozWifiP2pManager : EventTarget +{ + /** + * Enable/Disable wifi direct scan. + * + * onsuccess: Succeeded in starting/stopping wifi direct scan. + * onerror: Failed to start/stop wifi direct scan. + * + */ + DOMRequest setScanEnabled(boolean enabled); + + /** + * Connect to a peer with given configuration. + * + * @param address The peer MAC address we are going to connect. + * @param wpsMethod The WPS method we want to use. + * @param goIntent Number from 0 ~ 15 to indicate how much we want to be + * the group owner. + * + * onsuccess: Succeeded in issueing a 'connect' request. It doesn't mean we + * have connected to the peer. + * + * onerror: Failed to issue a 'connect' request, probably due to an + * invalid peer address, unsupported wps method or any + * preliminary error. + * + **/ + DOMRequest connect(DOMString address, WPSMethod wpsMethod, optional byte goIntent); + + /** + * Disconnect with a peer. + * + * @param address The mac address of the peer. + * + * onsuccess: Succeeded to issue a 'disconnect' request. It doesn't mean we + * have disconnected with the peer. + * + * onerror: Failed to issue a 'disconnect' request, probably due to the + * invalid peer address or any preliminary error. + * + */ + DOMRequest disconnect(DOMString address); + + /** + * Get peer list + * + * onsuccess: Command success, req.result contains an array of peer objects. + * onerror: Command failed. + * + * Peer object format: + * .address MAC address of the peer (string) + * .name the peer's device name (string) + * .isGroupOwner if the peer is the group owner (boolean) + * .wpsCapabilities array of the supported |WPSMethod| + * .connectionStatus one of { "disconnected", "connecting", "connected", "disconnecting" } + * + */ + DOMRequest getPeerList(); + + /** + * Set pairing confirmation result. + * + * @param accepted Boolean to indicate whether we accepted the request or not. + * @param pin The user input pin number if the wps method is keypad. + * + * onsuccess: Command succeeded. + * onerror: Command failed. + * + */ + DOMRequest setPairingConfirmation(boolean accepted, optional DOMString pin); + + /** + * Set device name. + * + * @param devieName The new device name we are going to set. + * + * onsuccess: Command succeeded. + * onerror: Command failed. + * + */ + DOMRequest setDeviceName(DOMString deviceName); + + /** + * Returns if Wifi Direct is enabled. + * + */ + readonly attribute boolean enabled; + + /** + * The current group owner, null if none. + */ + readonly attribute MozWifiP2pGroupOwner? groupOwner; + + /** + * An event listener that is called whenever the Wifi Direct peer list is + * updated. Use getPeerList() to get the up-to-date peer list. + */ + attribute EventHandler onpeerinfoupdate; + + /** + * An event listener that is called whenever Wifi Direct status changed. + * The address of the changed peer will be stored in event.peerList. + * See MozWifiP2pStatusChangeEvent.webidl. + */ + attribute EventHandler onstatuschange; + + /** + * An event listener that is called whenever Wifi Direct is enabled. + */ + attribute EventHandler onenabled; + + /** + * An event listener that is called whenever Wifi Direct is disabled. + */ + attribute EventHandler ondisabled; +}; diff --git a/dom/webidl/MozWifiP2pStatusChangeEvent.webidl b/dom/webidl/MozWifiP2pStatusChangeEvent.webidl new file mode 100644 index 000000000000..24fbc4b37d29 --- /dev/null +++ b/dom/webidl/MozWifiP2pStatusChangeEvent.webidl @@ -0,0 +1,16 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. + */ + +[Constructor(DOMString type, optional MozWifiP2pStatusChangeEventInit eventInitDict), HeaderFile="GeneratedEventClasses.h"] +interface MozWifiP2pStatusChangeEvent : Event +{ + readonly attribute DOMString peerAddress; +}; + +dictionary MozWifiP2pStatusChangeEventInit : EventInit +{ + DOMString peerAddress = ""; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 5571d05faba0..e7b8d814b819 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -537,6 +537,8 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk': WEBIDL_FILES += [ 'MozSpeakerManager.webidl', 'MozWifiConnectionInfoEvent.webidl', + 'MozWifiP2pManager.webidl', + 'MozWifiP2pStatusChangeEvent.webidl', 'MozWifiStatusChangeEvent.webidl', ] diff --git a/dom/wifi/DOMWifiP2pManager.js b/dom/wifi/DOMWifiP2pManager.js new file mode 100644 index 000000000000..a485035a0a58 --- /dev/null +++ b/dom/wifi/DOMWifiP2pManager.js @@ -0,0 +1,323 @@ +/* -*- 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/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +const DEBUG = false; + +// interface MozWifiP2pGroupOwner implementation. + +function MozWifiP2pGroupOwner(aGo) { + this.groupName = aGo.groupName; + this.macAddress = aGo.macAddress; + this.ipAddress = aGo.ipAddress; + this.passphrase = aGo.passphrase; + this.ssid = aGo.ssid; + this.wpsCapabilities = aGo.wpsCapabilities; + this.freq = aGo.freq; + this.isLocal = aGo.isLocal; +} + +MozWifiP2pGroupOwner.prototype = { + classID: Components.ID("{a9b81450-349d-11e3-aa6e-0800200c9a66}"), + contractID: "@mozilla.org/wifip2pgroupowner;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]) +}; + +// interface MozWifiP2pManager implementation. + +const MOZ_WIFIP2PMANAGER_CONTRACTID = "@mozilla.org/wifip2pmanager;1"; +const MOZ_WIFIP2PMANAGER_CID = Components.ID("{8d9125a0-3498-11e3-aa6e-0800200c9a66}"); + +function MozWifiP2pManager() { + this.defineEventHandlerGetterSetter("onstatuschange"); + this.defineEventHandlerGetterSetter("onpeerinfoupdate"); + this.defineEventHandlerGetterSetter("onenabled"); + this.defineEventHandlerGetterSetter("ondisabled"); + + this.currentPeer = null; + this.enabled = false; + this.groupOwner = null; +} + +// For smaller, read-only APIs, we expose any property that doesn't begin with +// an underscore. +function exposeReadOnly(obj) { + let exposedProps = {}; + for (let i in obj) { + if (i[0] === "_") { + continue; + } + exposedProps[i] = "r"; + } + + obj.__exposedProps__ = exposedProps; + return obj; +} + +function debug(msg) { + if (DEBUG) { + dump('-------------- MozWifiP2pManager: ' + msg); + } +} + +MozWifiP2pManager.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + classID: MOZ_WIFIP2PMANAGER_CID, + contractID: MOZ_WIFIP2PMANAGER_CONTRACTID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + Ci.nsISupports]), + + // + // nsIDOMGlobalPropertyInitializer implementation. + // + + init: function(aWindow) { + const messages = ["WifiP2pManager:setScanEnabled:Return:OK", + "WifiP2pManager:setScanEnabled:Return:NO", + "WifiP2pManager:getPeerList:Return:OK", + "WifiP2pManager:getPeerList:Return:NO", + "WifiP2pManager:connect:Return:OK", + "WifiP2pManager:connect:Return:NO", + "WifiP2pManager:disconnect:Return:OK", + "WifiP2pManager:disconnect:Return:NO", + "WifiP2pManager:setPairingConfirmation:Return", + "WifiP2pManager:setDeviceName:Return:OK", + "WifiP2pManager:setDeviceName:Return:NO", + + "WifiP2pManager:p2pDown", + "WifiP2pManager:p2pUp", + "WifiP2pManager:onconnecting", + "WifiP2pManager:onconnected", + "WifiP2pManager:ondisconnected", + "WifiP2pManager:ongroupnstop", + "WifiP2pManager:onconnectingfailed", + "WifiP2pManager:onwpstimeout", + "WifiP2pManager:onwpsfail", + "WifiP2pManager:onpeerinfoupdate", + ]; + + this.initDOMRequestHelper(aWindow, messages); + this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); + + // Notify the internal a new DOM mananger is created. + let state = this._mm.sendSyncMessage("WifiP2pManager:getState")[0]; + if (state) { + debug('State: ' + JSON.stringify(state)); + } else { + debug('Failed to get state'); + } + }, + + uninit: function() { + }, + + _sendMessageForRequest: function(name, data, request) { + let id = this.getRequestId(request); + this._mm.sendAsyncMessage(name, { data: data, rid: id, mid: this._id }); + }, + + receiveMessage: function(aMessage) { + let msg = aMessage.json; + if (msg.mid && msg.mid !== this._id) { + return; + } + + let request; + switch (aMessage.name) { + case "WifiP2pManager:setScanEnabled:Return:OK": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireSuccess(request, exposeReadOnly(msg.data)); + break; + + case "WifiP2pManager:setScanEnabled:Return:NO": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireError(request, "Unable to enable/disable Wifi P2P peer discovery."); + break; + + case "WifiP2pManager:getPeerList:Return:OK": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireSuccess(request, msg.data); + break; + + case "WifiP2pManager:getPeerList:Return:NO": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireError(request, "Unable to disable Wifi P2P peer discovery."); + break; + + case "WifiP2pManager:connect:Return:OK": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireSuccess(request, exposeReadOnly(msg.data)); + break; + + case "WifiP2pManager:connect:Return:NO": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireError(request, "Unable to connect to Wifi P2P peer."); + break; + + case "WifiP2pManager:disconnect:Return:OK": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireSuccess(request, exposeReadOnly(msg.data)); + break; + + case "WifiP2pManager:disconnect:Return:NO": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireError(request, "Unable to disconnect to Wifi P2P peer."); + break; + + case "WifiP2pManager:setDeviceName:Return:OK": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireSuccess(request, exposeReadOnly(msg.data)); + break; + + case "WifiP2pManager:setDeviceName:Return:NO": + request = this.takeRequest(msg.rid); + Services.DOMRequest.fireError(request, "Unable to set device name."); + break; + + case "WifiP2pManager:p2pDown": + this.enabled = false; + this.currentPeer = null; + this._fireEnabledOrDisabled(false); + break; + + case "WifiP2pManager:p2pUp": + this.enabled = true; + this._fireEnabledOrDisabled(true); + break; + + case "WifiP2pManager:onconnecting": + debug('onconnecting with peer: ' + JSON.stringify(msg.peer)); + this.currentPeer = msg.peer; + this._fireStatusChangeEvent(msg.peer.address); + break; + + case "WifiP2pManager:onconnected": + debug('onconnected with peer: ' + JSON.stringify(msg.peer)); + this.currentPeer = msg.peer; + this.groupOwner = new MozWifiP2pGroupOwner(msg.groupOwner); + this._fireStatusChangeEvent(msg.peer.address); + break; + + case "WifiP2pManager:ondisconnected": + debug('ondisconnected with peer: ' + JSON.stringify(msg.peer)); + this.currentPeer = null; + this.groupOwner = null; + this._fireStatusChangeEvent(msg.peer.address); + break; + + case "WifiP2pManager:onconnectingfailed": + this._fireStatusChangeEvent(null); + break; + + case "WifiP2pManager:onwpstimeout": + this._fireStatusChangeEvent(null); + break; + + case "WifiP2pManager:onwpsfail": + this._fireStatusChangeEvent(null); + break; + + case "WifiP2pManager:onpeerinfoupdate": + this._firePeerInfoUpdateEvent(); + break; + } + }, + + _firePeerInfoUpdateEvent: function PeerInfoUpdate() { + let evt = new this._window.Event("peerinfoupdate"); + this.__DOM_IMPL__.dispatchEvent(evt); + }, + + _fireStatusChangeEvent: function WifiP2pStatusChange(peerAddress) { + let evt = new this._window.MozWifiP2pStatusChangeEvent("statuschange", + { peerAddress: peerAddress }); + this.__DOM_IMPL__.dispatchEvent(evt); + }, + + _fireEnabledOrDisabled: function enabledDisabled(enabled) { + let evt = new this._window.Event(enabled ? "enabled" : "disabled"); + this.__DOM_IMPL__.dispatchEvent(evt); + }, + + // + // WifiP2pManager.webidl implementation. + // + + enableScan: function () { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:enableScan", null, request); + return request; + }, + + disableScan: function () { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:disableScan", null, request); + return request; + }, + + setScanEnabled: function(enabled) { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:setScanEnabled", enabled, request); + return request; + }, + + connect: function (address, wpsMethod, goIntent) { + let request = this.createRequest(); + let connectionInfo = { address: address, wpsMethod: wpsMethod, goIntent: goIntent }; + this._sendMessageForRequest("WifiP2pManager:connect", connectionInfo, request); + return request; + }, + + disconnect: function (address) { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:disconnect", address, request); + return request; + }, + + getPeerList: function () { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:getPeerList", null, request); + return request; + }, + + setPairingConfirmation: function (accepted, pin) { + let request = this.createRequest(); + let result = { accepted: accepted, pin: pin }; + this._sendMessageForRequest("WifiP2pManager:setPairingConfirmation", result, request); + return request; + }, + + setDeviceName: function(newDeviceName) { + let request = this.createRequest(); + this._sendMessageForRequest("WifiP2pManager:setDeviceName", newDeviceName, request); + return request; + }, + + // Helpers. + defineEventHandlerGetterSetter: function(event) { + Object.defineProperty(this, event, { + get: function() { + return this.__DOM_IMPL__.getEventHandler(event); + }, + + set: function(handler) { + this.__DOM_IMPL__.setEventHandler(event, handler); + } + }); + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozWifiP2pManager, MozWifiP2pGroupOwner]); diff --git a/dom/wifi/DOMWifiP2pManager.manifest b/dom/wifi/DOMWifiP2pManager.manifest new file mode 100644 index 000000000000..bc80efa489b9 --- /dev/null +++ b/dom/wifi/DOMWifiP2pManager.manifest @@ -0,0 +1,6 @@ +# DOMWifiP2pManager.js +component {8d9125a0-3498-11e3-aa6e-0800200c9a66} DOMWifiP2pManager.js +contract @mozilla.org/wifip2pmanager;1 {8d9125a0-3498-11e3-aa6e-0800200c9a66} + +component {a9b81450-349d-11e3-aa6e-0800200c9a66} DOMWifiP2pManager.js +contract @mozilla.org/wifip2pgroupowner;1 {a9b81450-349d-11e3-aa6e-0800200c9a66} \ No newline at end of file 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/WifiUtils.cpp b/dom/wifi/WifiUtils.cpp index a92ba15a2e66..9bd354156f6d 100644 --- a/dom/wifi/WifiUtils.cpp +++ b/dom/wifi/WifiUtils.cpp @@ -5,6 +5,7 @@ #include "WifiUtils.h" #include #include +#include #include "prinit.h" #include "js/CharacterEncoding.h" #include "NetUtils.h" @@ -34,6 +35,14 @@ GetSharedLibrary() return sWifiLib; } +static bool +GetWifiP2pSupported() +{ + char propP2pSupported[PROPERTY_VALUE_MAX]; + property_get("ro.moz.wifi.p2p_supported", propP2pSupported, "0"); + return (0 == strcmp(propP2pSupported, "1")); +} + // This is the same algorithm as in InflateUTF8StringToBuffer with Copy and // while ignoring invalids. // https://mxr.mozilla.org/mozilla-central/source/js/src/vm/CharacterEncoding.cpp#231 @@ -307,7 +316,7 @@ bool WpaSupplicant::ExecuteCommand(CommandOptions aOptions, } else if (aOptions.mCmd.EqualsLiteral("unload_driver")) { aResult.mStatus = mImpl->do_wifi_unload_driver(); } else if (aOptions.mCmd.EqualsLiteral("start_supplicant")) { - aResult.mStatus = mImpl->do_wifi_start_supplicant(0); + aResult.mStatus = mImpl->do_wifi_start_supplicant(GetWifiP2pSupported() ? 1 : 0); } else if (aOptions.mCmd.EqualsLiteral("stop_supplicant")) { aResult.mStatus = mImpl->do_wifi_stop_supplicant(0); } else if (aOptions.mCmd.EqualsLiteral("connect_to_supplicant")) { 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': diff --git a/dom/wifi/nsIDOMMozWifiP2pStatusChangeEvent.idl b/dom/wifi/nsIDOMMozWifiP2pStatusChangeEvent.idl new file mode 100644 index 000000000000..12c612578b58 --- /dev/null +++ b/dom/wifi/nsIDOMMozWifiP2pStatusChangeEvent.idl @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIDOMEvent.idl" + +[scriptable, builtinclass, uuid(82cad910-2019-11e3-8224-0800200c9a66)] +interface nsIDOMMozWifiP2pStatusChangeEvent : nsIDOMEvent +{ + /** + * The mac address of the peer whose status has just changed. + */ + readonly attribute DOMString peerAddress; + + [noscript] void initMozWifiP2pStatusChangeEvent(in DOMString aType, + in boolean aCanBubble, + in boolean aCancelable, + in DOMString aPeerAddress); +}; + +dictionary MozWifiP2pStatusChangeEventInit : EventInit +{ + DOMString peerAddress; +}; diff --git a/js/xpconnect/src/event_impl_gen.conf.in b/js/xpconnect/src/event_impl_gen.conf.in index cafb46e14458..ab71feb0a3f0 100644 --- a/js/xpconnect/src/event_impl_gen.conf.in +++ b/js/xpconnect/src/event_impl_gen.conf.in @@ -24,6 +24,7 @@ simple_events = [ 'StyleSheetChangeEvent', 'StyleSheetApplicableStateChangeEvent', #ifdef MOZ_WIDGET_GONK + 'MozWifiP2pStatusChangeEvent', 'MozWifiStatusChangeEvent', 'MozWifiConnectionInfoEvent', #endif diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index b073d844ec2c..5075056d7295 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -272,6 +272,12 @@ pref("media.mediasource.enabled", false); #ifdef MOZ_WEBSPEECH pref("media.webspeech.recognition.enable", false); #endif +#ifdef MOZ_WEBM_ENCODER +pref("media.encoder.webm.enabled", true); +#endif +#ifdef MOZ_OMX_ENCODER +pref("media.encoder.omx.enabled", true); +#endif // Whether to enable Web Audio support pref("media.webaudio.enabled", true); diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index 68d2a6154386..c1af92496a51 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -111,7 +111,7 @@ InternalMethods.prototype = { this.abortExistingFlow(); this.signedInUser = null; // clear in-memory cache return this.signedInUserStorage.set(null).then(() => { - this.notifyObservers("fxaccounts:onlogout"); + this.notifyObservers(ONLOGOUT_NOTIFICATION); }); }, @@ -198,7 +198,7 @@ InternalMethods.prototype = { // We are now ready for business. This should only be invoked once // per setSignedInUser(), regardless of whether we've rebooted since // setSignedInUser() was called. - internal.notifyObservers("fxaccounts:onlogin"); + internal.notifyObservers(ONLOGIN_NOTIFICATION); return data; }.bind(this)); }, @@ -347,7 +347,7 @@ InternalMethods.prototype = { }, notifyObservers: function(topic) { - log.debug("Notifying observers of user login"); + log.debug("Notifying observers of " + topic); Services.obs.notifyObservers(null, topic, null); }, diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js index 1c65449bcaee..474418b097e2 100644 --- a/services/fxaccounts/FxAccountsCommon.js +++ b/services/fxaccounts/FxAccountsCommon.js @@ -41,6 +41,10 @@ this.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours this.POLL_SESSION = 1000 * 60 * 5; // 5 minutes this.POLL_STEP = 1000 * 3; // 3 seconds +// Observer notifications. +this.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin"; +this.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout"; + // Server errno. // From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format this.ERRNO_ACCOUNT_ALREADY_EXISTS = 101; diff --git a/services/fxaccounts/FxAccountsManager.jsm b/services/fxaccounts/FxAccountsManager.jsm index cbae6cd0f959..9d664e9fb5c5 100644 --- a/services/fxaccounts/FxAccountsManager.jsm +++ b/services/fxaccounts/FxAccountsManager.jsm @@ -26,6 +26,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", this.FxAccountsManager = { + init: function() { + Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic !== ONLOGOUT_NOTIFICATION) { + return; + } + + // Remove the cached session if we get a logout notification. + this._activeSession = null; + }, + // We don't really need to save fxAccounts instance but this way we allow // to mock FxAccounts from tests. _fxAccounts: fxAccounts, @@ -156,22 +169,25 @@ this.FxAccountsManager = { return Promise.resolve(); } - return this._fxAccounts.signOut(this._activeSession.sessionToken).then( + // We clear the local session cache as soon as we get the onlogout + // notification triggered within FxAccounts.signOut, so we save the + // session token value to be able to remove the remote server session + // in case that we have network connection. + let sessionToken = this._activeSession.sessionToken; + + return this._fxAccounts.signOut(sessionToken).then( () => { - // If there is no connection, removing the local session should be - // enough. The client can create new sessions up to the limit (100?). + // At this point the local session should already be removed. + + // The client can create new sessions up to the limit (100?). // Orphaned tokens on the server will eventually be garbage collected. if (Services.io.offline) { - this._activeSession = null; return Promise.resolve(); } // Otherwise, we try to remove the remote session. let client = this._createFxAccountsClient(); - return client.signOut(this._activeSession.sessionToken).then( + return client.signOut(sessionToken).then( result => { - // Even if there is a remote server error, we remove the local - // session. - this._activeSession = null; let error = this._getError(result); if (error) { return Promise.reject({ @@ -183,9 +199,6 @@ this.FxAccountsManager = { return Promise.resolve(); }, reason => { - // Even if there is a remote server error, we remove the local - // session. - this._activeSession = null; return this._serverError(reason); } ); @@ -413,3 +426,5 @@ this.FxAccountsManager = { ); } }; + +FxAccountsManager.init(); diff --git a/services/fxaccounts/tests/xpcshell/test_manager.js b/services/fxaccounts/tests/xpcshell/test_manager.js index 0261f6826e99..cac8fcdda438 100644 --- a/services/fxaccounts/tests/xpcshell/test_manager.js +++ b/services/fxaccounts/tests/xpcshell/test_manager.js @@ -121,6 +121,7 @@ FxAccountsManager._fxAccounts = { signOut: function() { let deferred = Promise.defer(); this._signedInUser = null; + Services.obs.notifyObservers(null, ONLOGOUT_NOTIFICATION, null); deferred.resolve(); return deferred.promise; } @@ -581,3 +582,13 @@ add_test(function(test_queryAccount_no_accountId) { } ); }); + +add_test(function() { + do_print("= Test 23 | fxaccounts:onlogout notification ="); + do_check_true(FxAccountsManager._activeSession != null); + Services.obs.notifyObservers(null, ONLOGOUT_NOTIFICATION, null); + do_execute_soon(function() { + do_check_null(FxAccountsManager._activeSession); + run_next_test(); + }); +}); diff --git a/testing/marionette/marionette-listener.js b/testing/marionette/marionette-listener.js index 2ee073b5ced3..9794e095ab8c 100644 --- a/testing/marionette/marionette-listener.js +++ b/testing/marionette/marionette-listener.js @@ -40,6 +40,7 @@ let curFrame = content; let previousFrame = null; let elementManager = new ElementManager([]); let importedScripts = null; +let inputSource = null; // The sandbox we execute test scripts in. Gets lazily created in // createExecuteContentSandbox(). @@ -189,6 +190,12 @@ function newSession(msg) { resetValues(); if (isB2G) { readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT); + // We have to set correct mouse event source to MOZ_SOURCE_TOUCH + // to offer a way for event listeners to differentiate + // events being the result of a physical mouse action. + // This is especially important for the touch event shim, + // in order to prevent creating touch event for these fake mouse events. + inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH; } } @@ -664,7 +671,7 @@ function emitTouchEvent(type, touch) { marionetteLogObj.clearLogs(); */ let domWindowUtils = curFrame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); - domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.screenX], [touch.screenY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0); + domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0); } } @@ -672,10 +679,10 @@ function emitTouchEvent(type, touch) { * This function emit mouse event * @param: doc is the current document * type is the type of event to dispatch - * detail is the number of clicks, button notes the mouse button + * clickCount is the number of clicks, button notes the mouse button * elClientX and elClientY are the coordinates of the mouse relative to the viewport */ -function emitMouseEvent(doc, type, elClientX, elClientY, detail, button) { +function emitMouseEvent(doc, type, elClientX, elClientY, clickCount, button) { if (!wasInterrupted()) { let loggingInfo = "emitting Mouse event of type " + type + " at coordinates (" + elClientX + ", " + elClientY + ") relative to the viewport"; dumpLog(loggingInfo); @@ -686,11 +693,9 @@ function emitMouseEvent(doc, type, elClientX, elClientY, detail, button) { {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); marionetteLogObj.clearLogs(); */ - detail = detail || 1; - button = button || 0; let win = doc.defaultView; - // Figure out the element the mouse would be over at (x, y) - utils.synthesizeMouseAtPoint(elClientX, elClientY, {type: type, button: button, clickCount: detail}, win); + let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); + domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1, 0, false, 0, inputSource); } } @@ -1029,7 +1034,7 @@ function emitMultiEvents(type, touch, touches) { // Create changed touches let changedTouches = doc.createTouchList(touch); // Create the event object - let event = curFrame.document.createEvent('TouchEvent'); + let event = doc.createEvent('TouchEvent'); event.initTouchEvent(type, true, true, diff --git a/testing/mochitest/b2g.json b/testing/mochitest/b2g.json index 236646fafbe9..0549e5c63481 100644 --- a/testing/mochitest/b2g.json +++ b/testing/mochitest/b2g.json @@ -97,7 +97,6 @@ "content/base/test/test_XHRSendData.html":"seems to stall", "content/base/test/test_XHR_parameters.html":"86 total, 4 failing - testing mozAnon - got false, expected true", - "content/base/test/test_XHR_system.html":"12 total, 2 failing - .mozSystem == true - got false, expected true + ", "content/base/test/test_bug431701.html":"xmlhttprequest causes crash, bug 902271", "content/base/test/test_bug422537.html":"xmlhttprequest causes crash, bug 902271", @@ -238,18 +237,13 @@ "content/base/test/test_bug424359-2.html":"", "content/base/test/test_mixed_content_blocker_bug803225.html":"", "content/html/document/test/test_non-ascii-cookie.html":"", - "content/html/document/test/test_document.watch.html":"expects document.cookie setting to work", "docshell/test/navigation/test_bug13871.html":"", "docshell/test/navigation/test_bug270414.html":"", - "docshell/test/navigation/test_bug344861.html":"", - "docshell/test/navigation/test_bug386782.html":"", "docshell/test/navigation/test_not-opener.html":"", - "docshell/test/navigation/test_reserved.html":"", "docshell/test/test_bug413310.html":"", "dom/imptests/html/webgl":"", - "dom/battery/test/test_battery_basics.html":"", "dom/browser-element/mochitest/test_browserElement_inproc_Alert.html":"", "dom/browser-element/mochitest/test_browserElement_inproc_AppFramePermission.html":"", @@ -341,16 +335,10 @@ "dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html":"", "dom/media/tests/ipc/test_ipc.html":"nested ipc not working", - "dom/network/tests/test_networkstats_basics.html":"Will be fixed in bug 858005", "dom/permission/tests/test_permission_basics.html":"https not working, bug 907770", - "dom/tests/mochitest/bugs/test_bug335976.xhtml":"", - "dom/tests/mochitest/bugs/test_bug369306.html":"test timed out, can't focus back from popup window to opener?", - "dom/tests/mochitest/bugs/test_bug396843.html":"", "dom/tests/mochitest/bugs/test_bug406375.html":"", - "dom/tests/mochitest/bugs/test_bug427744.html":"", - "dom/tests/mochitest/bugs/test_bug641552.html":"", "dom/tests/mochitest/bugs/test_sizetocontent_clamp.html": "Windows can't change size on B2G", "dom/tests/mochitest/bugs/test_resize_move_windows.html": "Windows can't change size and position on B2G", "dom/tests/mochitest/bugs/test_window_bar.html":"", @@ -398,12 +386,10 @@ "dom/tests/mochitest/pointerlock/test_pointerlock-api.html":"window.open focus issues (using fullscreen)", "dom/tests/mochitest/webapps/test_bug_779982.html":"", - "dom/tests/mochitest/whatwg/test_postMessage_closed.html":"bug 894914 - wrong data - got FAIL, expected message", "dom/workers/test/test_suspend.html":"test timed out, might need more time", "dom/workers/test/test_xhr_parameters.html":"", "dom/workers/test/test_xhr_system.html":"", - "dom/workers/test/test_closeOnGC.html": "times out", "dom/workers/test/test_errorPropagation.html":"times out", "dom/workers/test/test_errorwarning.html":"Failed to load script: errorwarning_worker.js", "dom/workers/test/test_fibonacci.html":"Failed to load script: fibonacci_worker.js", @@ -415,22 +401,16 @@ "layout/base/tests/test_mozPaintCount.html":"depends on plugins support", "layout/forms/test/test_bug348236.html":"select form control popup", "layout/forms/test/test_bug446663.html":"needs copy support", - "layout/forms/test/test_bug564115.html":"times out on window.open and focus event", "layout/forms/test/test_bug571352.html":"shift-click multi-select not working?", "layout/forms/test/test_textarea_resize.html":"resizing textarea not available in b2g", "layout/forms/test/test_bug903715.html":"select elements don't use an in-page popup in B2G", - "layout/forms/test/test_listcontrol_search.html" : "select elements don't use an in-page popup in B2G", "layout/generic/test/test_bug392746.html":"ctrl mouse select not working in b2g", - "layout/generic/test/test_bug470212.html":"shift mouse select not working in b2g", - "layout/generic/test/test_bug514732.html":"times out, also on Android", "layout/generic/test/test_bug791616.html":"Target should not have scrolled - got 114.10000610351562, expected 115.39999389648438", "layout/generic/test/test_invalidate_during_plugin_paint.html":"plugins not supported", - "layout/generic/test/test_page_scroll_with_fixed_pos.html":"opened window too small?", "layout/generic/test/test_plugin_focus.html":"plugins not supported", "layout/generic/test/test_plugin_mouse_coords.html":"plugins not supported", "layout/generic/test/test_selection_expanding.html":"mouse selection not working", - "layout/style/test/test_rule_insertion.html":"monospace and serif text have sufficiently different widths", "layout/style/test/test_transitions_per_property.html":"times out, needs more time + various failures", "content/html/content/test/test_bug209275.xhtml":"timed out, 47 tests, bug 870262, :visited support", diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js index bc1564a04f15..4fb33590af15 100644 --- a/testing/mochitest/tests/SimpleTest/EventUtils.js +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -249,9 +249,13 @@ function synthesizeMouseAtPoint(left, top, aEvent, aWindow) var modifiers = _parseModifiers(aEvent); var pressure = ("pressure" in aEvent) ? aEvent.pressure : 0; var inputSource = ("inputSource" in aEvent) ? aEvent.inputSource : 0; + var synthesized = ("isSynthesized" in aEvent) ? aEvent.isSynthesized : true; if (("type" in aEvent) && aEvent.type) { - defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers, false, pressure, inputSource); + defaultPrevented = utils.sendMouseEvent(aEvent.type, left, top, button, + clickCount, modifiers, false, + pressure, inputSource, + synthesized); } else { utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource); diff --git a/toolkit/devtools/touch-events.js b/toolkit/devtools/touch-events.js index 72283d78c5ff..192cea32a55d 100644 --- a/toolkit/devtools/touch-events.js +++ b/toolkit/devtools/touch-events.js @@ -11,14 +11,19 @@ let handlerCount = 0; let orig_w3c_touch_events = Services.prefs.getIntPref('dom.w3c_touch_events.enabled'); +let trackedWindows = new WeakMap(); + // =================== Touch ==================== // Simulate touch events on desktop function TouchEventHandler (window) { + // Returns an already instanciated handler for this window + let cached = trackedWindows.get(window); + if (cached) { + return cached; + } + let contextMenuTimeout = 0; - // This guard is used to not re-enter the events processing loop for - // self dispatched events - let ignoreEvents = false; let threshold = 25; try { @@ -32,30 +37,35 @@ function TouchEventHandler (window) { let TouchEventHandler = { enabled: false, - events: ['mousedown', 'mousemove', 'mouseup', 'click'], + events: ['mousedown', 'mousemove', 'mouseup'], start: function teh_start() { - let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1; - handlerCount++; - Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1); + if (this.enabled) + return false; this.enabled = true; + let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1; + Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1); this.events.forEach((function(evt) { - window.addEventListener(evt, this, true); + // Only listen trusted events to prevent messing with + // event dispatched manually within content documents + window.addEventListener(evt, this, true, false); }).bind(this)); return isReloadNeeded; }, stop: function teh_stop() { - handlerCount--; - if (handlerCount == 0) - Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events); + if (!this.enabled) + return; this.enabled = false; + Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events); this.events.forEach((function(evt) { window.removeEventListener(evt, this, true); }).bind(this)); }, handleEvent: function teh_handleEvent(evt) { - if (evt.button || ignoreEvents || - evt.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN) + // Ignore all but real mouse event coming from physical mouse + // (especially ignore mouse event being dispatched from a touch event) + if (evt.button || evt.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE || evt.isSynthesized) { return; + } // The gaia system window use an hybrid system even on the device which is // a mix of mouse/touch events. So let's not cancel *all* mouse events @@ -105,6 +115,13 @@ function TouchEventHandler (window) { content.clearTimeout(contextMenuTimeout); type = 'touchend'; + + // Only register click listener after mouseup to ensure + // catching only real user click. (Especially ignore click + // being dispatched on form submit) + if (evt.detail == 1) { + window.addEventListener('click', this, true, false); + } break; case 'click': @@ -113,6 +130,8 @@ function TouchEventHandler (window) { evt.preventDefault(); evt.stopImmediatePropagation(); + window.removeEventListener('click', this, true, false); + if (this.cancelClick) return; @@ -141,7 +160,7 @@ function TouchEventHandler (window) { let content = this.getContent(evt.target); var utils = content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); - utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true); + utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true, 0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); }, sendContextMenu: function teh_sendContextMenu(target, x, y, delay) { let doc = target.ownerDocument; @@ -159,6 +178,26 @@ function TouchEventHandler (window) { return timeout; }, sendTouchEvent: function teh_sendTouchEvent(evt, target, name) { + // When running OOP b2g desktop, we need to send the touch events + // using the mozbrowser api on the unwrapped frame. + if (target.localName == "iframe" && target.mozbrowser === true) { + if (name == "touchstart") { + this.touchstartTime = Date.now(); + } else if (name == "touchend") { + // If we have a 'fast' tap, don't send a click as both will be turned + // into a click and that breaks eg. checkboxes. + if (Date.now() - this.touchstartTime < delay) { + this.cancelClick = true; + } + } + let unwraped = XPCNativeWrapper.unwrap(target); + unwraped.sendTouchEvent(name, [0], // event type, id + [evt.clientX], [evt.clientY], // x, y + [1], [1], // rx, ry + [0], [0], // rotation, force + 1); // count + return; + } let document = target.ownerDocument; let content = this.getContent(target); @@ -179,15 +218,10 @@ function TouchEventHandler (window) { }, getContent: function teh_getContent(target) { let win = target.ownerDocument.defaultView; - let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem); - let topDocShell = docShell.sameTypeRootTreeItem; - topDocShell.QueryInterface(Ci.nsIDocShell); - let top = topDocShell.contentViewer.DOMDocument.defaultView; - return top; + return win; } }; + trackedWindows.set(window, TouchEventHandler); return TouchEventHandler; } diff --git a/widget/TextEvents.h b/widget/TextEvents.h index 7fd7d55e2193..92138f713839 100644 --- a/widget/TextEvents.h +++ b/widget/TextEvents.h @@ -32,6 +32,8 @@ enum #undef NS_DEFINE_VK +#define kLatestSeqno UINT32_MAX + namespace mozilla { namespace dom { @@ -166,18 +168,25 @@ private: friend class plugins::PPluginInstanceChild; WidgetTextEvent() + : mSeqno(kLatestSeqno) + , rangeCount(0) + , rangeArray(nullptr) + , isChar(false) { } public: - uint32_t seqno; + uint32_t mSeqno; public: virtual WidgetTextEvent* AsTextEvent() MOZ_OVERRIDE { return this; } - WidgetTextEvent(bool aIsTrusted, uint32_t aMessage, nsIWidget* aWidget) : - WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_TEXT_EVENT), - rangeCount(0), rangeArray(nullptr), isChar(false) + WidgetTextEvent(bool aIsTrusted, uint32_t aMessage, nsIWidget* aWidget) + : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_TEXT_EVENT) + , mSeqno(kLatestSeqno) + , rangeCount(0) + , rangeArray(nullptr) + , isChar(false) { } @@ -217,11 +226,12 @@ private: friend class mozilla::dom::PBrowserChild; WidgetCompositionEvent() + : mSeqno(kLatestSeqno) { } public: - uint32_t seqno; + uint32_t mSeqno; public: virtual WidgetCompositionEvent* AsCompositionEvent() MOZ_OVERRIDE @@ -230,8 +240,9 @@ public: } WidgetCompositionEvent(bool aIsTrusted, uint32_t aMessage, - nsIWidget* aWidget) : - WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_COMPOSITION_EVENT) + nsIWidget* aWidget) + : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_COMPOSITION_EVENT) + , mSeqno(kLatestSeqno) { // XXX compositionstart is cancelable in draft of DOM3 Events. // However, it doesn't make sense for us, we cannot cancel composition @@ -376,12 +387,18 @@ private: friend class mozilla::dom::PBrowserChild; WidgetSelectionEvent() + : mSeqno(kLatestSeqno) + , mOffset(0) + , mLength(0) + , mReversed(false) + , mExpandToClusterBoundary(true) + , mSucceeded(false) { MOZ_CRASH("WidgetSelectionEvent is created without proper arguments"); } public: - uint32_t seqno; + uint32_t mSeqno; public: virtual WidgetSelectionEvent* AsSelectionEvent() MOZ_OVERRIDE @@ -389,9 +406,14 @@ public: return this; } - WidgetSelectionEvent(bool aIsTrusted, uint32_t aMessage, nsIWidget* aWidget) : - WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_SELECTION_EVENT), - mExpandToClusterBoundary(true), mSucceeded(false) + WidgetSelectionEvent(bool aIsTrusted, uint32_t aMessage, nsIWidget* aWidget) + : WidgetGUIEvent(aIsTrusted, aMessage, aWidget, NS_SELECTION_EVENT) + , mSeqno(kLatestSeqno) + , mOffset(0) + , mLength(0) + , mReversed(false) + , mExpandToClusterBoundary(true) + , mSucceeded(false) { } diff --git a/widget/nsGUIEventIPC.h b/widget/nsGUIEventIPC.h index 55980a74e046..22d326110454 100644 --- a/widget/nsGUIEventIPC.h +++ b/widget/nsGUIEventIPC.h @@ -386,7 +386,7 @@ struct ParamTraits static void Write(Message* aMsg, const paramType& aParam) { WriteParam(aMsg, static_cast(aParam)); - WriteParam(aMsg, aParam.seqno); + WriteParam(aMsg, aParam.mSeqno); WriteParam(aMsg, aParam.theText); WriteParam(aMsg, aParam.isChar); WriteParam(aMsg, aParam.rangeCount); @@ -398,7 +398,7 @@ struct ParamTraits { if (!ReadParam(aMsg, aIter, static_cast(aResult)) || - !ReadParam(aMsg, aIter, &aResult->seqno) || + !ReadParam(aMsg, aIter, &aResult->mSeqno) || !ReadParam(aMsg, aIter, &aResult->theText) || !ReadParam(aMsg, aIter, &aResult->isChar) || !ReadParam(aMsg, aIter, &aResult->rangeCount)) @@ -436,7 +436,7 @@ struct ParamTraits static void Write(Message* aMsg, const paramType& aParam) { WriteParam(aMsg, static_cast(aParam)); - WriteParam(aMsg, aParam.seqno); + WriteParam(aMsg, aParam.mSeqno); WriteParam(aMsg, aParam.data); } @@ -444,7 +444,7 @@ struct ParamTraits { return ReadParam(aMsg, aIter, static_cast(aResult)) && - ReadParam(aMsg, aIter, &aResult->seqno) && + ReadParam(aMsg, aIter, &aResult->mSeqno) && ReadParam(aMsg, aIter, &aResult->data); } }; @@ -493,7 +493,7 @@ struct ParamTraits static void Write(Message* aMsg, const paramType& aParam) { WriteParam(aMsg, static_cast(aParam)); - WriteParam(aMsg, aParam.seqno); + WriteParam(aMsg, aParam.mSeqno); WriteParam(aMsg, aParam.mOffset); WriteParam(aMsg, aParam.mLength); WriteParam(aMsg, aParam.mReversed); @@ -505,7 +505,7 @@ struct ParamTraits { return ReadParam(aMsg, aIter, static_cast(aResult)) && - ReadParam(aMsg, aIter, &aResult->seqno) && + ReadParam(aMsg, aIter, &aResult->mSeqno) && ReadParam(aMsg, aIter, &aResult->mOffset) && ReadParam(aMsg, aIter, &aResult->mLength) && ReadParam(aMsg, aIter, &aResult->mReversed) && diff --git a/widget/xpwidgets/PuppetWidget.cpp b/widget/xpwidgets/PuppetWidget.cpp index 211bd3b384e0..326bebeca301 100644 --- a/widget/xpwidgets/PuppetWidget.cpp +++ b/widget/xpwidgets/PuppetWidget.cpp @@ -273,25 +273,26 @@ PuppetWidget::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus) if (event->message == NS_COMPOSITION_START) { mIMEComposing = true; } + uint32_t seqno = kLatestSeqno; switch (event->eventStructType) { case NS_COMPOSITION_EVENT: - mIMELastReceivedSeqno = event->AsCompositionEvent()->seqno; - if (mIMELastReceivedSeqno < mIMELastBlurSeqno) - return NS_OK; + seqno = event->AsCompositionEvent()->mSeqno; break; case NS_TEXT_EVENT: - mIMELastReceivedSeqno = event->AsTextEvent()->seqno; - if (mIMELastReceivedSeqno < mIMELastBlurSeqno) - return NS_OK; + seqno = event->AsTextEvent()->mSeqno; break; case NS_SELECTION_EVENT: - mIMELastReceivedSeqno = event->AsSelectionEvent()->seqno; - if (mIMELastReceivedSeqno < mIMELastBlurSeqno) - return NS_OK; + seqno = event->AsSelectionEvent()->mSeqno; break; default: break; } + if (seqno != kLatestSeqno) { + mIMELastReceivedSeqno = seqno; + if (mIMELastReceivedSeqno < mIMELastBlurSeqno) { + return NS_OK; + } + } if (mAttachedWidgetListener) { aStatus = mAttachedWidgetListener->HandleEvent(event, mUseAttachedEvents); @@ -352,7 +353,7 @@ PuppetWidget::IMEEndComposition(bool aCancel) nsEventStatus status; WidgetTextEvent textEvent(true, NS_TEXT_TEXT, this); InitEvent(textEvent, nullptr); - textEvent.seqno = mIMELastReceivedSeqno; + textEvent.mSeqno = mIMELastReceivedSeqno; // SendEndIMEComposition is always called since ResetInputState // should always be called even if we aren't composing something. if (!mTabChild || @@ -367,7 +368,7 @@ PuppetWidget::IMEEndComposition(bool aCancel) WidgetCompositionEvent compEvent(true, NS_COMPOSITION_END, this); InitEvent(compEvent, nullptr); - compEvent.seqno = mIMELastReceivedSeqno; + compEvent.mSeqno = mIMELastReceivedSeqno; DispatchEvent(&compEvent, status); return NS_OK; }